import { describe, it, expect, vi, beforeEach } from 'vitest';
import { KIEClient } from './kie-client.js';
import type { KIECreateTaskRequest, KIETaskResponse } from '../../types/api.js';

const mockFetch = vi.fn();
vi.mock('undici', () => ({ fetch: (...args: unknown[]) => mockFetch(...args) }));

vi.mock('../../utils/logger.js', () => ({
  createModuleLogger: () => ({
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }),
}));

vi.mock('../../utils/config.js', () => ({
  config: { kieApiKey: 'test-kie-key' },
}));

function makeOkResponse(data: unknown) {
  return {
    ok: true,
    status: 200,
    statusText: 'OK',
    json: () => Promise.resolve({ code: 200, data }),
    text: () => Promise.resolve(JSON.stringify({ code: 200, data })),
  };
}

function makeErrorResponse(status: number, body = 'error') {
  return {
    ok: false,
    status,
    statusText: 'Error',
    json: () => Promise.resolve({ error: body }),
    text: () => Promise.resolve(body),
  };
}

const SAMPLE_TASK_RESPONSE: KIETaskResponse = {
  taskId: 'task-abc-123',
  status: 'pending',
};

const SAMPLE_REQUEST: KIECreateTaskRequest = {
  model: 'flux-2',
  prompt: 'A portrait photo of a woman',
  aspectRatio: '1:1',
};

describe('KIEClient', () => {
  let client: KIEClient;

  beforeEach(() => {
    client = new KIEClient('test-kie-key', { initialBackoffMs: 1 });
    mockFetch.mockReset();
  });

  describe('constructor', () => {
    it('allows empty API key for deferred configuration', () => {
      expect(new KIEClient('')).toBeInstanceOf(KIEClient);
    });

    it('creates instance with valid key', () => {
      expect(new KIEClient('valid-key')).toBeInstanceOf(KIEClient);
    });
  });

  describe('createTask', () => {
    it('sends correct POST request and returns response', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const result = await client.createTask(SAMPLE_REQUEST);

      expect(result).toEqual(SAMPLE_TASK_RESPONSE);
      expect(mockFetch).toHaveBeenCalledOnce();

      const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit];
      expect(url).toBe('https://api.kie.ai/api/v1/jobs/createTask');
      expect(options.method).toBe('POST');

      const headers = options.headers as Record<string, string>;
      expect(headers['Authorization']).toBe('Bearer test-kie-key');
      expect(headers['Content-Type']).toBe('application/json');

      const body = JSON.parse(options.body as string);
      expect(body.model).toBe('flux-2');
      expect(body.input.prompt).toBe('A portrait photo of a woman');
      expect(body.input.aspect_ratio).toBe('1:1');
    });

    it('sends all optional fields when provided', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const fullRequest: KIECreateTaskRequest = {
        model: 'seedream-4-5',
        prompt: 'test prompt',
        negativePrompt: 'blurry, low quality',
        referenceImages: ['https://example.com/img1.jpg', 'https://example.com/img2.jpg'],
        aspectRatio: '16:9',
        webhookUrl: 'https://example.com/webhook',
        enableTranslation: true,
        seed: 42,
      };

      await client.createTask(fullRequest);

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.input.negative_prompt).toBe('blurry, low quality');
      expect(body.input.reference_images).toHaveLength(2);
      expect(body.callBackUrl).toBe('https://example.com/webhook');
      expect(body.input.enable_translation).toBe(true);
      expect(body.input.seed).toBe(42);
    });

    it('supports all valid models', async () => {
      const models: KIECreateTaskRequest['model'][] = [
        'nano-banana-2', 'flux-2', 'seedream-4-5', 'z-image', '4o-image',
      ];

      for (const model of models) {
        mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));
        await expect(client.createTask({ ...SAMPLE_REQUEST, model })).resolves.toEqual(SAMPLE_TASK_RESPONSE);
      }
    });

    it('supports all valid aspect ratios', async () => {
      const ratios: KIECreateTaskRequest['aspectRatio'][] = ['1:1', '16:9', '9:16', '4:3', '3:4'];

      for (const aspectRatio of ratios) {
        mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));
        await expect(client.createTask({ ...SAMPLE_REQUEST, aspectRatio })).resolves.toEqual(SAMPLE_TASK_RESPONSE);
      }
    });

    it('allows up to 8 reference images', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const request: KIECreateTaskRequest = {
        ...SAMPLE_REQUEST,
        referenceImages: Array.from({ length: 8 }, (_, i) => `https://example.com/img${i}.jpg`),
      };

      await expect(client.createTask(request)).resolves.toEqual(SAMPLE_TASK_RESPONSE);
    });

    it('rejects more than 8 reference images', async () => {
      const request: KIECreateTaskRequest = {
        ...SAMPLE_REQUEST,
        referenceImages: Array.from({ length: 9 }, (_, i) => `https://example.com/img${i}.jpg`),
      };

      await expect(client.createTask(request)).rejects.toThrow('referenceImages exceeds maximum of 8');
    });

    it('rejects empty prompt', async () => {
      await expect(client.createTask({ ...SAMPLE_REQUEST, prompt: '' })).rejects.toThrow('prompt is required');
    });

    it('rejects invalid model', async () => {
      await expect(
        client.createTask({ ...SAMPLE_REQUEST, model: 'invalid-model' as KIECreateTaskRequest['model'] }),
      ).rejects.toThrow('Invalid model');
    });

    it('rejects invalid aspect ratio', async () => {
      await expect(
        client.createTask({ ...SAMPLE_REQUEST, aspectRatio: '2:1' as KIECreateTaskRequest['aspectRatio'] }),
      ).rejects.toThrow('Invalid aspectRatio');
    });
  });

  describe('getTaskStatus', () => {
    it('sends correct GET request with taskId', async () => {
      const completedResponse: KIETaskResponse = {
        taskId: 'task-abc-123',
        status: 'completed',
        resultUrl: 'https://cdn.kie.ai/result.jpg',
      };
      mockFetch.mockResolvedValueOnce(makeOkResponse(completedResponse));

      const result = await client.getTaskStatus('task-abc-123');

      expect(result).toEqual(completedResponse);
      const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit];
      expect(url).toBe('https://api.kie.ai/api/v1/jobs/recordInfo?taskId=task-abc-123');
      expect(options.method).toBe('GET');

      const headers = options.headers as Record<string, string>;
      expect(headers['Authorization']).toBe('Bearer test-kie-key');
    });

    it('encodes taskId in URL', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

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

      const [url] = mockFetch.mock.calls[0] as [string];
      expect(url).toBe('https://api.kie.ai/api/v1/jobs/recordInfo?taskId=task%20with%20spaces');
    });

    it('rejects empty taskId', async () => {
      await expect(client.getTaskStatus('')).rejects.toThrow('taskId is required');
    });

    it('returns failed task status with error', async () => {
      const failedResponse: KIETaskResponse = {
        taskId: 'task-fail',
        status: 'failed',
        error: 'Content policy violation',
      };
      mockFetch.mockResolvedValueOnce(makeOkResponse(failedResponse));

      const result = await client.getTaskStatus('task-fail');
      expect(result.status).toBe('failed');
      expect(result.error).toBe('Content policy violation');
    });
  });

  describe('retry logic', () => {
    it('retries on 500 errors with exponential backoff', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(502))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const result = await client.createTask(SAMPLE_REQUEST);
      expect(result).toEqual(SAMPLE_TASK_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('retries on 429 rate limit errors', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(429, 'rate limited'))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const result = await client.createTask(SAMPLE_REQUEST);
      expect(result).toEqual(SAMPLE_TASK_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('throws immediately on 4xx client errors (not 429)', async () => {
      mockFetch.mockResolvedValueOnce(makeErrorResponse(400, 'bad request'));

      await expect(client.createTask(SAMPLE_REQUEST)).rejects.toThrow('KIE API error: 400');
      expect(mockFetch).toHaveBeenCalledOnce();
    });

    it('throws after exhausting all retries', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(500));

      await expect(client.createTask(SAMPLE_REQUEST)).rejects.toThrow('KIE API error: 500');
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('retries on network errors', async () => {
      mockFetch
        .mockRejectedValueOnce(new Error('fetch failed'))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const result = await client.createTask(SAMPLE_REQUEST);
      expect(result).toEqual(SAMPLE_TASK_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('retries getTaskStatus on server errors', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(503))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_TASK_RESPONSE));

      const result = await client.getTaskStatus('task-123');
      expect(result).toEqual(SAMPLE_TASK_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });
  });
});

describe('createKIEClient', () => {
  it('creates client from config', async () => {
    const { createKIEClient } = await import('./kie-client.js');
    const client = createKIEClient();
    expect(client).toBeInstanceOf(KIEClient);
  });
});
