/**
 * Tests for @bytelyst/extraction package — client factory + types.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createExtractionClient } from '../client.js';
import type {
  ExtractRequest,
  ExtractResponse,
  BatchExtractRequest,
  BatchExtractResponse,
  ExtractionTask,
  ExtractionClientConfig,
  ExtractionEntity,
  ExtractionExample,
} from '../types.js';

// ── Mock @bytelyst/api-client ──────────────────────────────────

const mockApiFetch = vi.fn();

vi.mock('@bytelyst/api-client', () => ({
  createApiClient: vi.fn(() => ({
    fetch: mockApiFetch,
  })),
}));

describe('createExtractionClient', () => {
  beforeEach(() => {
    mockApiFetch.mockReset();
  });

  it('returns an object with extract, extractBatch, listTasks, getTask', () => {
    const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
    expect(typeof client.extract).toBe('function');
    expect(typeof client.extractBatch).toBe('function');
    expect(typeof client.listTasks).toBe('function');
    expect(typeof client.getTask).toBe('function');
  });

  describe('extract', () => {
    it('calls POST /api/extract with correct body', async () => {
      const mockResponse: ExtractResponse = {
        extractions: [{ extraction_class: 'person', extraction_text: 'John' }],
        metadata: { modelId: 'gemini-1.5', durationMs: 150, charCount: 35 },
      };
      mockApiFetch.mockResolvedValue(mockResponse);

      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
      const req: ExtractRequest = {
        text: 'John said we should ship by Friday.',
        taskId: 'transcript-extraction',
      };

      const result = await client.extract(req);

      expect(mockApiFetch).toHaveBeenCalledWith('/api/extract', {
        method: 'POST',
        body: JSON.stringify(req),
      });
      expect(result).toEqual(mockResponse);
    });

    it('passes optional fields in request', async () => {
      mockApiFetch.mockResolvedValue({ extractions: [], metadata: {} });
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      const req: ExtractRequest = {
        text: 'Hello world',
        taskPrompt: 'Extract entities',
        modelId: 'gpt-4',
        productId: 'lysnrai',
        options: { extractionPasses: 2, maxWorkers: 4, maxCharBuffer: 1000 },
        examples: [
          { text: 'Hi Bob', extractions: [{ extraction_class: 'person', extraction_text: 'Bob' }] },
        ],
      };

      await client.extract(req);
      expect(mockApiFetch).toHaveBeenCalledWith('/api/extract', {
        method: 'POST',
        body: JSON.stringify(req),
      });
    });

    it('propagates errors from api client', async () => {
      mockApiFetch.mockRejectedValue(new Error('Forbidden'));
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await expect(client.extract({ text: 'test' })).rejects.toThrow('Forbidden');
    });
  });

  describe('extractBatch', () => {
    it('calls POST /api/extract/batch with correct body', async () => {
      const mockResponse: BatchExtractResponse = {
        results: [{ extractions: [], metadata: { modelId: 'test', durationMs: 10, charCount: 5 } }],
        requestId: 'req-123',
      };
      mockApiFetch.mockResolvedValue(mockResponse);

      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
      const req: BatchExtractRequest = {
        inputs: [
          { text: 'First document', taskId: 'triage' },
          { text: 'Second document', taskPrompt: 'Extract names' },
        ],
        modelId: 'gemini-1.5',
        productId: 'mindlyst',
      };

      const result = await client.extractBatch(req);

      expect(mockApiFetch).toHaveBeenCalledWith('/api/extract/batch', {
        method: 'POST',
        body: JSON.stringify(req),
      });
      expect(result).toEqual(mockResponse);
    });
  });

  describe('listTasks', () => {
    it('calls GET /api/tasks without productId', async () => {
      const tasks: ExtractionTask[] = [
        {
          id: 'triage',
          name: 'Triage Extraction',
          prompt: 'Extract entities',
          classes: ['person', 'date'],
          builtIn: true,
          productId: 'lysnrai',
        },
      ];
      mockApiFetch.mockResolvedValue(tasks);

      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
      const result = await client.listTasks();

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks');
      expect(result).toEqual(tasks);
    });

    it('appends productId query param when provided', async () => {
      mockApiFetch.mockResolvedValue([]);
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await client.listTasks('mindlyst');

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks?productId=mindlyst');
    });

    it('encodes special characters in productId', async () => {
      mockApiFetch.mockResolvedValue([]);
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await client.listTasks('my product');

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks?productId=my%20product');
    });
  });

  describe('getTask', () => {
    it('calls GET /api/tasks/:id without productId', async () => {
      const task: ExtractionTask = {
        id: 'triage',
        name: 'Triage',
        prompt: 'Extract',
        classes: ['person'],
        builtIn: true,
        productId: 'lysnrai',
      };
      mockApiFetch.mockResolvedValue(task);

      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
      const result = await client.getTask('triage');

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks/triage');
      expect(result).toEqual(task);
    });

    it('appends productId query param when provided', async () => {
      mockApiFetch.mockResolvedValue({});
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await client.getTask('my-task', 'mindlyst');

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks/my-task?productId=mindlyst');
    });

    it('encodes special characters in task id', async () => {
      mockApiFetch.mockResolvedValue({});
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await client.getTask('task with spaces');

      expect(mockApiFetch).toHaveBeenCalledWith('/api/tasks/task%20with%20spaces');
    });
  });

  describe('transcribe', () => {
    it('calls POST /api/transcribe with correct body', async () => {
      const mockResponse = {
        text: 'Hello, this is a test recording.',
        language: 'en',
        durationSeconds: 5.2,
        model: 'whisper-1',
        durationMs: 1200,
      };
      mockApiFetch.mockResolvedValue(mockResponse);

      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });
      const req = {
        audioUrl: 'https://blob.example.com/audio.mp3',
        language: 'en',
        productId: 'notelett',
      };

      const result = await client.transcribe(req);

      expect(mockApiFetch).toHaveBeenCalledWith('/api/transcribe', {
        method: 'POST',
        body: JSON.stringify(req),
      });
      expect(result.text).toBe('Hello, this is a test recording.');
      expect(result.language).toBe('en');
      expect(result.durationSeconds).toBe(5.2);
    });

    it('passes optional model and prompt fields', async () => {
      mockApiFetch.mockResolvedValue({
        text: 'test',
        language: null,
        durationSeconds: null,
        model: 'whisper-1',
        durationMs: 100,
      });
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      const req = {
        audioUrl: 'https://blob.example.com/meeting.wav',
        model: 'whisper-1',
        prompt: 'Technical meeting about software architecture.',
        responseFormat: 'verbose_json' as const,
      };

      await client.transcribe(req);
      expect(mockApiFetch).toHaveBeenCalledWith('/api/transcribe', {
        method: 'POST',
        body: JSON.stringify(req),
      });
    });

    it('propagates errors from api client', async () => {
      mockApiFetch.mockRejectedValue(new Error('Service unavailable'));
      const client = createExtractionClient({ baseUrl: 'http://localhost:4005' });

      await expect(
        client.transcribe({ audioUrl: 'https://blob.example.com/audio.mp3' })
      ).rejects.toThrow('Service unavailable');
    });
  });

  describe('config options', () => {
    it('passes getToken to createApiClient', async () => {
      const { createApiClient } = await import('@bytelyst/api-client');
      const getToken = () => 'my-token';

      createExtractionClient({ baseUrl: 'http://localhost:4005', getToken });

      expect(createApiClient).toHaveBeenCalledWith({
        baseUrl: 'http://localhost:4005',
        getToken,
      });
    });

    it('works without getToken', async () => {
      const { createApiClient } = await import('@bytelyst/api-client');

      createExtractionClient({ baseUrl: 'http://localhost:4005' });

      expect(createApiClient).toHaveBeenCalledWith({
        baseUrl: 'http://localhost:4005',
        getToken: undefined,
      });
    });
  });
});

describe('Extraction types', () => {
  it('ExtractionEntity shape is correct', () => {
    const entity: ExtractionEntity = {
      extraction_class: 'person',
      extraction_text: 'John Doe',
      attributes: { role: 'engineer' },
      start_offset: 0,
      end_offset: 8,
    };
    expect(entity.extraction_class).toBe('person');
    expect(entity.attributes?.role).toBe('engineer');
  });

  it('ExtractionExample shape is correct', () => {
    const example: ExtractionExample = {
      text: 'Meet Bob at 3pm',
      extractions: [
        { extraction_class: 'person', extraction_text: 'Bob' },
        { extraction_class: 'time', extraction_text: '3pm' },
      ],
    };
    expect(example.extractions).toHaveLength(2);
  });

  it('ExtractionClientConfig with optional getToken', () => {
    const config1: ExtractionClientConfig = { baseUrl: 'http://localhost:4005' };
    expect(config1.getToken).toBeUndefined();

    const config2: ExtractionClientConfig = {
      baseUrl: 'http://localhost:4005',
      getToken: () => 'tok',
    };
    expect(config2.getToken?.()).toBe('tok');
  });
});
