import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TelegramBotClient } from './bot-client.js';
import type { TelegramBotInfo, SendPaidMediaRequest } from '../../types/telegram.js';
import type { TelegramApiResponse } from './bot-client.js';

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

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

function makeOkResponse<T>(result: T): { ok: true; status: number; json: () => Promise<TelegramApiResponse<T>> } {
  return {
    ok: true,
    status: 200,
    json: () => Promise.resolve({ ok: true, result }),
  };
}

function makeApiErrorResponse(errorCode: number, description: string) {
  return {
    ok: true,
    status: 200,
    json: () =>
      Promise.resolve({
        ok: false,
        error_code: errorCode,
        description,
      }),
  };
}

const BOT_INFO: TelegramBotInfo = {
  id: 123456789,
  is_bot: true,
  first_name: 'TestBot',
  username: 'test_bot',
};

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

  beforeEach(() => {
    client = new TelegramBotClient('test-token-123', { initialBackoffMs: 1 });
    mockFetch.mockReset();
  });

  describe('constructor', () => {
    it('throws if token is empty', () => {
      expect(() => new TelegramBotClient('')).toThrow('Telegram bot token is required');
    });

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

  describe('getMe', () => {
    it('returns bot info on success', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(BOT_INFO));

      const result = await client.getMe();

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

      const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit];
      expect(url).toBe('https://api.telegram.org/bottest-token-123/getMe');
      expect(options.method).toBe('POST');
    });
  });

  describe('sendMessage', () => {
    it('sends message with correct params', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse({ message_id: 1 }));

      await client.sendMessage(12345, 'Hello!');

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.chat_id).toBe(12345);
      expect(body.text).toBe('Hello!');
    });

    it('includes optional params when provided', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse({ message_id: 1 }));

      await client.sendMessage(12345, 'Hello!', {
        parse_mode: 'HTML',
        reply_to_message_id: 99,
      });

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.parse_mode).toBe('HTML');
      expect(body.reply_to_message_id).toBe(99);
    });
  });

  describe('sendChatAction', () => {
    it('sends typing action', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      const result = await client.sendChatAction(12345, 'typing');

      expect(result).toBe(true);
      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.chat_id).toBe(12345);
      expect(body.action).toBe('typing');
    });
  });

  describe('sendPaidMedia', () => {
    it('sends paid media request', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse({ message_id: 2 }));

      const request: SendPaidMediaRequest = {
        chat_id: 12345,
        star_count: 50,
        media: [{ type: 'photo', media: 'https://example.com/photo.jpg' }],
        caption: 'Exclusive content',
      };

      await client.sendPaidMedia(request);

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.chat_id).toBe(12345);
      expect(body.star_count).toBe(50);
      expect(body.media).toEqual([{ type: 'photo', media: 'https://example.com/photo.jpg' }]);
      expect(body.caption).toBe('Exclusive content');
    });
  });

  describe('createChatInviteLink', () => {
    it('creates invite link with options', async () => {
      const inviteLink = { invite_link: 'https://t.me/+abc123' };
      mockFetch.mockResolvedValueOnce(makeOkResponse(inviteLink));

      const result = await client.createChatInviteLink(-100123, {
        name: 'VIP Link',
        member_limit: 100,
      });

      expect(result).toEqual(inviteLink);
      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.chat_id).toBe(-100123);
      expect(body.name).toBe('VIP Link');
      expect(body.member_limit).toBe(100);
    });
  });

  describe('setWebhook', () => {
    it('sets webhook URL', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      const result = await client.setWebhook('https://example.com/webhook');

      expect(result).toBe(true);
      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.url).toBe('https://example.com/webhook');
    });

    it('includes optional webhook options', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      await client.setWebhook('https://example.com/webhook', {
        max_connections: 40,
        allowed_updates: ['message', 'pre_checkout_query'],
        secret_token: 'secret-123',
      });

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.max_connections).toBe(40);
      expect(body.allowed_updates).toEqual(['message', 'pre_checkout_query']);
      expect(body.secret_token).toBe('secret-123');
    });
  });

  describe('deleteWebhook', () => {
    it('deletes webhook', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      const result = await client.deleteWebhook();

      expect(result).toBe(true);
      const [url] = mockFetch.mock.calls[0] as [string, RequestInit];
      expect(url).toBe('https://api.telegram.org/bottest-token-123/deleteWebhook');
    });
  });

  describe('answerPreCheckoutQuery', () => {
    it('answers pre-checkout query with ok=true', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      const result = await client.answerPreCheckoutQuery('query-123', true);

      expect(result).toBe(true);
      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.pre_checkout_query_id).toBe('query-123');
      expect(body.ok).toBe(true);
      expect(body).not.toHaveProperty('error_message');
    });

    it('answers pre-checkout query with ok=false and error message', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(true));

      await client.answerPreCheckoutQuery('query-456', false, 'Item unavailable');

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.pre_checkout_query_id).toBe('query-456');
      expect(body.ok).toBe(false);
      expect(body.error_message).toBe('Item unavailable');
    });
  });

  describe('retry logic', () => {
    it('retries on 500 errors with exponential backoff', async () => {
      mockFetch
        .mockResolvedValueOnce(makeApiErrorResponse(500, 'Internal Server Error'))
        .mockResolvedValueOnce(makeApiErrorResponse(502, 'Bad Gateway'))
        .mockResolvedValueOnce(makeOkResponse(BOT_INFO));

      const result = await client.getMe();
      expect(result).toEqual(BOT_INFO);
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('retries on 429 rate limit errors', async () => {
      mockFetch
        .mockResolvedValueOnce(makeApiErrorResponse(429, 'Too Many Requests'))
        .mockResolvedValueOnce(makeOkResponse(true));

      const result = await client.sendChatAction(123, 'typing');
      expect(result).toBe(true);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('throws immediately on 4xx client errors (not 429)', async () => {
      mockFetch.mockResolvedValueOnce(makeApiErrorResponse(400, 'Bad Request'));

      await expect(client.getMe()).rejects.toThrow('Telegram API error: 400');
      expect(mockFetch).toHaveBeenCalledOnce();
    });

    it('throws after exhausting all retries', async () => {
      mockFetch
        .mockResolvedValueOnce(makeApiErrorResponse(500, 'Internal Server Error'))
        .mockResolvedValueOnce(makeApiErrorResponse(500, 'Internal Server Error'))
        .mockResolvedValueOnce(makeApiErrorResponse(500, 'Internal Server Error'));

      await expect(client.getMe()).rejects.toThrow('Telegram API error: 500');
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

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

      const result = await client.getMe();
      expect(result).toEqual(BOT_INFO);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('throws after all network error retries exhausted', async () => {
      mockFetch
        .mockRejectedValueOnce(new Error('fetch failed'))
        .mockRejectedValueOnce(new Error('fetch failed'))
        .mockRejectedValueOnce(new Error('fetch failed'));

      await expect(client.getMe()).rejects.toThrow('fetch failed');
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });
  });
});
