import { describe, it, expect, vi, beforeEach } from 'vitest';
import { StarsManager } from './stars-manager.js';
import type { StarsTransaction } from '../../types/api.js';
import type { PreCheckoutQuery, SuccessfulPayment } from '../../types/telegram.js';

vi.mock('uuid', () => {
  let counter = 0;
  return { v4: () => `uuid-${++counter}` };
});

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

function createMockDataStore() {
  const transactions: Map<string, StarsTransaction[]> = new Map();
  return {
    saveStarsTransaction: vi.fn(async (tx: StarsTransaction) => {
      const list = transactions.get(tx.characterId) ?? [];
      list.push(tx);
      transactions.set(tx.characterId, list);
    }),
    getStarsTransactions: vi.fn(async (characterId: string) => {
      return [...(transactions.get(characterId) ?? [])];
    }),
    _transactions: transactions,
    _clear: () => transactions.clear(),
  };
}

function createMockBotClient() {
  return {
    answerPreCheckoutQuery: vi.fn(async () => true),
    sendPaidMedia: vi.fn(async () => ({})),
    sendMessage: vi.fn(async () => ({})),
  };
}

const CHAR_ID = 'char-001';
const USER_ID = 12345;

function makePreCheckoutQuery(overrides?: Partial<PreCheckoutQuery>): PreCheckoutQuery {
  return {
    id: 'pchk-1',
    from: { id: USER_ID, is_bot: false, first_name: 'Test' },
    currency: 'XTR',
    total_amount: 50,
    invoice_payload: JSON.stringify({ imageId: 'img_abc' }),
    ...overrides,
  };
}

function makeSuccessfulPayment(overrides?: Partial<SuccessfulPayment>): SuccessfulPayment {
  return {
    currency: 'XTR',
    total_amount: 50,
    invoice_payload: JSON.stringify({ imageId: 'img_abc' }),
    telegram_payment_charge_id: 'tg_charge_1',
    provider_payment_charge_id: 'prov_charge_1',
    ...overrides,
  };
}

describe('StarsManager', () => {
  let manager: StarsManager;
  let mockStore: ReturnType<typeof createMockDataStore>;
  let mockBot: ReturnType<typeof createMockBotClient>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockStore = createMockDataStore();
    mockBot = createMockBotClient();
    manager = new StarsManager(mockStore as any);
  });

  describe('handlePreCheckout', () => {
    it('should approve pre-checkout query and create pending transaction', async () => {
      const query = makePreCheckoutQuery();
      await manager.handlePreCheckout(CHAR_ID, query, mockBot as any);

      expect(mockBot.answerPreCheckoutQuery).toHaveBeenCalledWith('pchk-1', true);
      expect(mockStore.saveStarsTransaction).toHaveBeenCalledTimes(1);

      const savedTx = mockStore.saveStarsTransaction.mock.calls[0][0] as StarsTransaction;
      expect(savedTx.characterId).toBe(CHAR_ID);
      expect(savedTx.telegramUserId).toBe(USER_ID);
      expect(savedTx.type).toBe('payment');
      expect(savedTx.starCount).toBe(50);
      expect(savedTx.status).toBe('pending');
    });

    it('should throw if answerPreCheckoutQuery fails', async () => {
      mockBot.answerPreCheckoutQuery.mockRejectedValueOnce(new Error('API error'));
      const query = makePreCheckoutQuery();

      await expect(manager.handlePreCheckout(CHAR_ID, query, mockBot as any)).rejects.toThrow(
        'API error',
      );
    });
  });

  describe('handleSuccessfulPayment', () => {
    it('should create completed transaction and send paid media', async () => {
      const payment = makeSuccessfulPayment();
      const tx = await manager.handleSuccessfulPayment(CHAR_ID, payment, USER_ID, mockBot as any);

      expect(tx.status).toBe('completed');
      expect(tx.telegramPaymentId).toBe('tg_charge_1');
      expect(tx.imageId).toBe('img_abc');
      expect(mockBot.sendPaidMedia).toHaveBeenCalledWith({
        chat_id: USER_ID,
        star_count: 50,
        media: [{ type: 'photo', media: 'img_abc' }],
      });
    });

    it('should not send paid media when no imageId in payload', async () => {
      const payment = makeSuccessfulPayment({ invoice_payload: 'no-json' });
      const tx = await manager.handleSuccessfulPayment(CHAR_ID, payment, USER_ID, mockBot as any);

      expect(tx.status).toBe('completed');
      expect(mockBot.sendPaidMedia).not.toHaveBeenCalled();
    });

    it('should parse img_ prefixed payload as imageId', async () => {
      const payment = makeSuccessfulPayment({ invoice_payload: 'img_xyz123' });
      const tx = await manager.handleSuccessfulPayment(CHAR_ID, payment, USER_ID, mockBot as any);

      expect(tx.imageId).toBe('img_xyz123');
      expect(mockBot.sendPaidMedia).toHaveBeenCalled();
    });

    it('should still complete transaction if sendPaidMedia fails', async () => {
      mockBot.sendPaidMedia.mockRejectedValueOnce(new Error('send failed'));
      const payment = makeSuccessfulPayment();
      const tx = await manager.handleSuccessfulPayment(CHAR_ID, payment, USER_ID, mockBot as any);

      expect(tx.status).toBe('completed');
    });
  });

  describe('handleStarReaction', () => {
    it('should create a completed reaction transaction', async () => {
      const tx = await manager.handleStarReaction(CHAR_ID, USER_ID, 5);

      expect(tx.type).toBe('reaction');
      expect(tx.starCount).toBe(5);
      expect(tx.status).toBe('completed');
      expect(tx.characterId).toBe(CHAR_ID);
      expect(tx.telegramUserId).toBe(USER_ID);
    });
  });

  describe('publishPaidMedia', () => {
    it('should call sendPaidMedia on the bot client', async () => {
      await manager.publishPaidMedia(CHAR_ID, '-1001234', 'img_001', 100, mockBot as any, 'VIP content');

      expect(mockBot.sendPaidMedia).toHaveBeenCalledWith({
        chat_id: -1001234,
        star_count: 100,
        media: [{ type: 'photo', media: 'img_001' }],
        caption: 'VIP content',
      });
    });
  });

  describe('getTransactions', () => {
    async function seedTransactions() {
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'payment', starCount: 50, status: 'completed' });
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'reaction', starCount: 5, status: 'completed' });
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: 999, type: 'payment', starCount: 30, status: 'pending' });
    }

    it('should return all transactions sorted by createdAt descending', async () => {
      await seedTransactions();
      const txs = await manager.getTransactions(CHAR_ID);

      expect(txs).toHaveLength(3);
      for (let i = 1; i < txs.length; i++) {
        expect(new Date(txs[i - 1].createdAt).getTime()).toBeGreaterThanOrEqual(
          new Date(txs[i].createdAt).getTime(),
        );
      }
    });

    it('should filter by type', async () => {
      await seedTransactions();
      const txs = await manager.getTransactions(CHAR_ID, { type: 'reaction' });
      expect(txs).toHaveLength(1);
      expect(txs[0].type).toBe('reaction');
    });

    it('should filter by status', async () => {
      await seedTransactions();
      const txs = await manager.getTransactions(CHAR_ID, { status: 'pending' });
      expect(txs).toHaveLength(1);
      expect(txs[0].status).toBe('pending');
    });

    it('should support limit and offset', async () => {
      await seedTransactions();
      const txs = await manager.getTransactions(CHAR_ID, { limit: 1, offset: 1 });
      expect(txs).toHaveLength(1);
    });
  });

  describe('getRevenue', () => {
    it('should calculate total revenue from completed transactions', async () => {
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'payment', starCount: 100, status: 'completed' });
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'reaction', starCount: 10, status: 'completed' });
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'payment', starCount: 50, status: 'pending' });

      const revenue = await manager.getRevenue(CHAR_ID);

      expect(revenue.totalStars).toBe(110);
      expect(revenue.totalTransactions).toBe(2);
      expect(revenue.byType.payment).toBe(100);
      expect(revenue.byType.reaction).toBe(10);
    });

    it('should return zero revenue for character with no transactions', async () => {
      const revenue = await manager.getRevenue('nonexistent');
      expect(revenue.totalStars).toBe(0);
      expect(revenue.totalTransactions).toBe(0);
    });

    it('should filter by daily period', async () => {
      await manager.createTransaction({ characterId: CHAR_ID, telegramUserId: USER_ID, type: 'payment', starCount: 50, status: 'completed' });

      const revenue = await manager.getRevenue(CHAR_ID, 'daily');
      expect(revenue.totalStars).toBe(50);
    });
  });

  describe('createTransaction', () => {
    it('should create a transaction with generated id and timestamp', async () => {
      const tx = await manager.createTransaction({
        characterId: CHAR_ID,
        telegramUserId: USER_ID,
        type: 'payment',
        starCount: 75,
      });

      expect(tx.id).toBeDefined();
      expect(tx.createdAt).toBeDefined();
      expect(tx.status).toBe('pending');
      expect(tx.starCount).toBe(75);
      expect(mockStore.saveStarsTransaction).toHaveBeenCalledWith(tx);
    });

    it('should use provided status when given', async () => {
      const tx = await manager.createTransaction({
        characterId: CHAR_ID,
        telegramUserId: USER_ID,
        type: 'payment',
        starCount: 25,
        status: 'completed',
      });

      expect(tx.status).toBe('completed');
    });
  });
});
