import * as crypto from 'crypto';
import { type ConfigService } from '@nestjs/config';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MomoService } from '../services/momo.service';

describe('MomoService', () => {
  let service: MomoService;
  const secretKey = 'TESTSECRETKEY123456789012345678';
  const partnerCode = 'TESTPARTNER';
  const accessKey = 'TESTACCESSKEY';
  const callbackBaseUrl = 'https://api.goodgo.vn';

  beforeEach(() => {
    const mockConfig = {
      get: vi.fn((key: string, defaultValue?: string) => {
        const env: Record<string, string> = {
          'MOMO_PARTNER_CODE': 'TESTPARTNER',
          'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
          'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
          'PAYMENT_CALLBACK_BASE_URL': callbackBaseUrl,
        };
        return env[key] ?? defaultValue;
      }),
      getOrThrow: vi.fn((key: string) => {
        const env: Record<string, string> = {
          'MOMO_PARTNER_CODE': 'TESTPARTNER',
          'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
          'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
        };
        if (!env[key]) throw new Error(`Missing ${key}`);
        return env[key];
      }),
    } as unknown as ConfigService;
    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
    service = new MomoService(mockConfig, mockLogger as any);
  });

  function buildCallbackData(overrides: Record<string, string> = {}): Record<string, string> {
    const data: Record<string, string> = {
      amount: '500000',
      extraData: '',
      message: 'Success',
      orderId: 'order-123',
      orderInfo: 'Test payment',
      orderType: 'momo_wallet',
      partnerCode,
      payType: 'qr',
      requestId: 'req-123',
      responseTime: '1700000000000',
      resultCode: '0',
      transId: 'MOMO_TX_123',
      ...overrides,
    };

    const rawSignature = [
      `accessKey=${accessKey}`,
      `amount=${data['amount']}`,
      `extraData=${data['extraData']}`,
      `message=${data['message']}`,
      `orderId=${data['orderId']}`,
      `orderInfo=${data['orderInfo']}`,
      `orderType=${data['orderType']}`,
      `partnerCode=${data['partnerCode']}`,
      `payType=${data['payType']}`,
      `requestId=${data['requestId']}`,
      `responseTime=${data['responseTime']}`,
      `resultCode=${data['resultCode']}`,
      `transId=${data['transId']}`,
    ].join('&');

    data['signature'] = crypto
      .createHmac('sha256', secretKey)
      .update(rawSignature)
      .digest('hex');

    return data;
  }

  it('should verify a valid callback with timingSafeEqual', () => {
    const data = buildCallbackData();
    const result = service.verifyCallback(data);

    expect(result.isValid).toBe(true);
    expect(result.isSuccess).toBe(true);
    expect(result.orderId).toBe('order-123');
    expect(result.providerTxId).toBe('MOMO_TX_123');
  });

  it('should reject an invalid signature', () => {
    const data = buildCallbackData();
    // Tamper the signature — replace with a same-length hex string
    data['signature'] = 'a'.repeat(data['signature']!.length);

    const result = service.verifyCallback(data);
    expect(result.isValid).toBe(false);
    expect(result.isSuccess).toBe(false);
  });

  it('should reject an empty signature', () => {
    const data = buildCallbackData();
    data['signature'] = '';

    const result = service.verifyCallback(data);
    expect(result.isValid).toBe(false);
  });

  it('should reject a signature with wrong length', () => {
    const data = buildCallbackData();
    data['signature'] = 'abcdef1234'; // too short

    const result = service.verifyCallback(data);
    expect(result.isValid).toBe(false);
  });

  it('should detect failed payment (non-zero resultCode) with valid signature', () => {
    const data = buildCallbackData({ resultCode: '1006' });

    const result = service.verifyCallback(data);
    expect(result.isValid).toBe(true);
    expect(result.isSuccess).toBe(false);
  });

  it('createPaymentUrl should use backend IPN URL for ipnUrl and frontend URL for redirectUrl', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      json: async () => ({ resultCode: 0, payUrl: 'https://pay.momo.vn/abc' }),
    });
    vi.stubGlobal('fetch', fetchMock);

    const returnUrl = 'https://goodgo.vn/payment/return';
    await service.createPaymentUrl({
      orderId: 'order-xyz',
      amountVND: 100000n,
      description: 'Test',
      returnUrl,
      ipAddress: '127.0.0.1',
    });

    expect(fetchMock).toHaveBeenCalledOnce();
    const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
    expect(body.redirectUrl).toBe(returnUrl);
    expect(body.ipnUrl).toBe(`${callbackBaseUrl}/api/v1/payments/callback/momo`);
    expect(body.ipnUrl).not.toBe(body.redirectUrl);

    vi.unstubAllGlobals();
  });

  it('createPaymentUrl should use provided callbackUrl as ipnUrl when supplied', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      json: async () => ({ resultCode: 0, payUrl: 'https://pay.momo.vn/abc' }),
    });
    vi.stubGlobal('fetch', fetchMock);

    const customCallback = 'https://staging-api.goodgo.vn/api/v1/payments/callback/momo';
    await service.createPaymentUrl({
      orderId: 'order-xyz',
      amountVND: 100000n,
      description: 'Test',
      returnUrl: 'https://goodgo.vn/payment/return',
      callbackUrl: customCallback,
      ipAddress: '127.0.0.1',
    });

    const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
    expect(body.ipnUrl).toBe(customCallback);

    vi.unstubAllGlobals();
  });
});
