import { Request, Response } from 'express';
import { AutoRechargeConfig, Customer, MeterEvent, PaymentCurrency } from '../../src/store/models';
import { checkTokenBalance } from '../../src/libs/payment';
import { getPriceUintAmountByCurrency } from '../../src/libs/price';
import logger from '../../src/libs/logger';
import creditGrantsRoute from '../../src/routes/credit-grants';

// Mock dependencies
jest.mock('../../src/libs/payment');
jest.mock('../../src/libs/price');
jest.mock('../../src/libs/logger', () => ({
  error: jest.fn(),
  info: jest.fn(),
  warn: jest.fn(),
  debug: jest.fn(),
}));

// Note: Models are not fully mocked to allow spying on their methods

describe('GET /api/credit-grants/verify-availability', () => {
  let mockReq: Partial<Request>;
  let mockRes: Partial<Response>;
  let routeHandler: any;

  beforeEach(() => {
    // Setup mocks
    jest.clearAllMocks();

    mockReq = {
      query: {
        customer_id: 'customer_123',
        currency_id: 'currency_123',
      },
      livemode: true,
    } as any;

    mockRes = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn().mockReturnThis(),
    } as any;

    // Extract route handler from router
    // The route structure is: router.get('/verify-availability', authMine, handler)
    // We'll get the handler by finding the route in the router stack
    try {
      const router = creditGrantsRoute as any;
      const routeLayer = router.stack.find((layer: any) => {
        return layer.route && layer.route.path === '/verify-availability';
      });
      if (routeLayer && routeLayer.route) {
        // The handler is the last middleware in the stack (after authMine)
        const { stack } = routeLayer.route;
        routeHandler = stack[stack.length - 1]?.handle;
      }
    } catch (error) {
      // If we can't extract the handler, we'll skip these tests
      console.warn('Could not extract route handler:', error);
    }
  });

  describe('Input validation', () => {
    it('should return 400 if customer_id is missing', async () => {
      if (!routeHandler) return;
      mockReq.query = { currency_id: 'currency_123' } as any;

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.stringContaining('customer_id'),
        })
      );
    });

    it('should return 400 if currency_id is missing', async () => {
      if (!routeHandler) return;
      mockReq.query = { customer_id: 'customer_123' } as any;

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith(
        expect.objectContaining({
          error: expect.stringContaining('currency_id'),
        })
      );
    });
  });

  describe('Customer and currency checks', () => {
    it('should return 404 if customer not found', async () => {
      if (!routeHandler) return;
      jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue(null as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: 'Customer customer_123 not found',
      });
    });

    it('should return 404 if currency not found', async () => {
      if (!routeHandler) return;
      jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123' } as any);
      jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue(null as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: 'PaymentCurrency currency_123 not found',
      });
    });
  });

  describe('Auto recharge config checks', () => {
    beforeEach(() => {
      jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123' } as any);
      jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
        id: 'currency_123',
        decimal: 2,
      } as any);
    });

    it('should return can_continue: false if auto recharge config not found', async () => {
      if (!routeHandler) return;
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(null as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        has_auto_recharge: false,
        reason: 'auto_recharge_config_not_found',
      });
    });

    it('should return can_continue: false if recharge currency not found', async () => {
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
        rechargeCurrency: null,
        price: { id: 'price_123' },
        paymentMethod: { id: 'pm_123' },
      } as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'recharge_currency_not_found',
      });
    });

    it('should return can_continue: false if price not found', async () => {
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: null,
        paymentMethod: { id: 'pm_123' },
      } as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'price_not_found',
      });
    });

    it('should return can_continue: false if payment method not found', async () => {
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: { id: 'price_123' },
        paymentMethod: null,
      } as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'payment_method_not_found',
      });
    });

    it('should return can_continue: false if payment method is stripe', async () => {
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: { id: 'price_123' },
        paymentMethod: { type: 'stripe' },
      } as any);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'balance_check_not_supported',
      });
    });

    it('should return can_continue: false if price amount is invalid', async () => {
      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: { id: 'price_123' },
        paymentMethod: { type: 'arcblock' },
        quantity: 1,
      } as any);
      (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue(null);

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'invalid_price_amount',
      });
    });
  });

  describe('No pending amount scenarios', () => {
    let mockConfig: any;

    beforeEach(() => {
      jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: 'did:customer:123' } as any);
      jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
        id: 'currency_123',
        decimal: 2,
      } as any);

      mockConfig = {
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: {
          id: 'price_123',
          metadata: {
            credit_config: {
              credit_amount: '1',
            },
          },
        },
        paymentMethod: {
          type: 'arcblock',
          getOcapClient: jest.fn(),
        },
        quantity: 1,
        payment_settings: {
          payment_method_options: {
            arcblock: {
              payer: 'did:test:123',
            },
          },
        },
      };

      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
      (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit with 6 decimals for ABT
      jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
    });

    it('should return can_continue: true if balance is sufficient for one recharge', async () => {
      (checkTokenBalance as jest.Mock).mockResolvedValue({
        sufficient: true,
        token: { balance: '6000000' }, // 60 with 2 decimals
      });

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(checkTokenBalance).toHaveBeenCalledWith(
        expect.objectContaining({
          amount: '1000000', // totalAmount for one recharge (1 credit)
        })
      );
      expect(mockRes.json).toHaveBeenCalledWith(
        expect.objectContaining({
          can_continue: true,
          payment_account_sufficient: true,
        })
      );
    });

    it('should return can_continue: true if balance exactly equals one recharge amount (boundary)', async () => {
      // Balance: exactly 0.1 ABT (one recharge cost for 1 credit)
      (checkTokenBalance as jest.Mock)
        .mockResolvedValueOnce({
          sufficient: true,
          token: { balance: '1000000' }, // Exactly one recharge
        })
        .mockResolvedValueOnce({
          sufficient: true,
          token: { balance: '1000000' },
        });

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(checkTokenBalance).toHaveBeenCalledWith(
        expect.objectContaining({
          amount: '1000000',
        })
      );
      expect(mockRes.json).toHaveBeenCalledWith(
        expect.objectContaining({
          can_continue: true,
          payment_account_sufficient: true,
        })
      );
    });

    it('should return can_continue: false if balance is insufficient for one recharge', async () => {
      (checkTokenBalance as jest.Mock).mockReset();
      (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
        if (params.amount === '1000000') {
          return {
            sufficient: false,
            token: { balance: '500000' }, // 0.05 ABT, insufficient for 0.1 ABT
            reason: 'NO_ENOUGH_TOKEN',
          };
        }
        return {
          sufficient: true,
          token: { balance: '500000' },
        };
      });

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'insufficient_balance',
        payment_account_balance: '500000',
        pending_amount: '0',
        required_amount: '1000000',
      });
    });

    it('should return can_continue: false if balance is exactly one unit less than required (boundary)', async () => {
      // Balance: 0.0999999 ABT (one unit less than 0.1 ABT)
      (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
        if (params.amount === '1000000') {
          return {
            sufficient: false,
            token: { balance: '999999' }, // 1 unit less than required
            reason: 'NO_ENOUGH_TOKEN',
          };
        }
        return {
          sufficient: true,
          token: { balance: '999999' },
        };
      });

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'insufficient_balance',
        payment_account_balance: '999999',
        pending_amount: '0',
        required_amount: '1000000',
      });
    });

    it('should return can_continue: false if balance is zero', async () => {
      // Balance: 0 ABT
      (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
        if (params.amount === '1000000') {
          return {
            sufficient: false,
            token: { balance: '0' },
            reason: 'NO_ENOUGH_TOKEN',
          };
        }
        return {
          sufficient: true,
          token: { balance: '0' },
        };
      });

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.json).toHaveBeenCalledWith({
        can_continue: false,
        reason: 'insufficient_balance',
        payment_account_balance: '0',
        pending_amount: '0',
        required_amount: '1000000',
      });
    });
  });

  describe('Pending amount scenarios', () => {
    let mockConfig: any;

    beforeEach(() => {
      jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: 'did:customer:123' } as any);
      jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
        id: 'currency_123',
        decimal: 2,
      } as any);

      mockConfig = {
        rechargeCurrency: { id: 'recharge_currency_123' },
        price: {
          id: 'price_123',
          metadata: {
            credit_config: {
              credit_amount: '1', // 1 credit per recharge
            },
          },
        },
        paymentMethod: {
          type: 'arcblock',
          getOcapClient: jest.fn(),
        },
        quantity: 1,
        payment_settings: {
          payment_method_options: {
            arcblock: {
              payer: 'did:test:123',
            },
          },
        },
        last_recharge_date: '2024-01-01',
      };

      jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
      (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit with 2 decimals = 0.1 ABT (1 * 10^6 for ABT decimal=6)
    });

    describe('Recharge times limit (3 times)', () => {
      it('should return can_continue: false if required recharge times > 3', async () => {
        // Pending: 4 credits, each recharge: 1 credit, need 4 times
        // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'too_many_recharges_required',
          pending_amount: '400',
          required_recharge_times: '4',
          max_allowed_times: 3,
        });
      });

      it('should return can_continue: false if required recharge times exactly equals 4 (boundary)', async () => {
        // Pending: 4 credits, each recharge: 1 credit, need ceil(4/1) = 4 times
        // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'too_many_recharges_required',
          pending_amount: '400',
          required_recharge_times: '4',
          max_allowed_times: 3,
        });
      });

      it('should continue if required recharge times exactly equals 3 (boundary)', async () => {
        // Pending: 3 credits, each recharge: 1 credit, need exactly 3 times
        // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'too_many_recharges_required',
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });

      it('should continue if required recharge times is 1 (minimal pending)', async () => {
        // Pending: 1 credit, each recharge: 1 credit, need 1 time
        // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'too_many_recharges_required',
          })
        );
        expect(checkTokenBalance).toHaveBeenCalled();
      });

      it('should continue if required recharge times <= 3', async () => {
        // Pending: 2 credits, each recharge: 1 credit, need 2 times
        // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should not return too_many_recharges_required error
        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'too_many_recharges_required',
          })
        );
      });
    });

    describe('Custom max_recharge_times parameter', () => {
      it('should use custom max_recharge_times when provided', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_recharge_times: '5', // Custom: allow up to 5 recharges
        } as any;

        // Pending: 4 credits, each recharge: 1 credit, need 4 times
        // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '30000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should pass because 4 < 5
        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'too_many_recharges_required',
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });

      it('should reject if required recharge times exceeds custom max_recharge_times', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_recharge_times: '2', // Custom: only allow 2 recharges
        } as any;

        // Pending: 3 credits, each recharge: 1 credit, need 3 times
        // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should fail because 3 > 2
        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'too_many_recharges_required',
          pending_amount: '300',
          required_recharge_times: '3',
          max_allowed_times: 2,
        });
      });

      it('should use default max_recharge_times (3) when not provided', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          // max_recharge_times not provided, should default to 3
        };

        // Pending: 4 credits, each recharge: 1 credit, need 4 times
        // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should fail because 4 > 3 (default)
        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'too_many_recharges_required',
          pending_amount: '400',
          required_recharge_times: '4',
          max_allowed_times: 3, // Default value
        });
      });
    });

    describe('Max pending amount limit', () => {
      it('should reject if pending amount exceeds max_pending_amount', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_pending_amount: '3', // Token format: limit to 3 credits
        };

        // Pending: 4 credits (in unit format with decimal=2: 4 * 10^2 = 400, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'pending_amount_exceeds_limit',
          pending_amount: '400',
          max_pending_amount: '300', // Converted to unit format: 3 * 10^2 = 300
          detail: 'Current pending amount exceeds the maximum allowed limit',
        });
      });

      it('should allow if pending amount equals max_pending_amount (boundary)', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_pending_amount: '4', // Token format: limit to 4 credits
        };

        // Pending: exactly 4 credits (in unit format: 4 * 10^2 = 400, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should not be rejected by max_pending_amount check
        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'pending_amount_exceeds_limit',
          })
        );
      });

      it('should allow if pending amount is below max_pending_amount', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_pending_amount: '4', // Token format: limit to 4 credits
        };

        // Pending: 2 credits (in unit format: 2 * 10^2 = 200, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'pending_amount_exceeds_limit',
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });

      it('should handle decimal max_pending_amount values', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_pending_amount: '3.5', // Token format with decimal: 3.5 credits
        };

        // Pending: 4 credits (in unit format: 4 * 10^2 = 400, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'pending_amount_exceeds_limit',
          pending_amount: '400',
          max_pending_amount: '350', // Converted: 3.5 * 10^2 = 350
          detail: 'Current pending amount exceeds the maximum allowed limit',
        });
      });

      it('should not check max_pending_amount if pending is zero', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          max_pending_amount: '10', // Very low limit
        };

        // No pending amount
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should not be rejected even though limit is low, because there's no pending
        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'pending_amount_exceeds_limit',
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });
    });

    describe('Balance checks with pending amount', () => {
      beforeEach(() => {
        // Pending: 2 credits, each recharge: 1 credit, need 2 times
        // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
      });

      it('should return can_continue: false if balance is insufficient (less than required)', async () => {
        // Pending: 2 credits, each recharge: 1 credit
        // Required: 2 * 0.1 ABT = 0.2 ABT (pay off pending) + 0.1 ABT (one more recharge) = 0.3 ABT
        // Balance: 0.25 ABT (insufficient)
        (checkTokenBalance as jest.Mock).mockReset();
        (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
          // Required amount: 200 (pending) + 100 (one more recharge) = 300 (unit format for credits)
          // Convert to ABT: need to check actual amount calculation
          const requiredAmount = '3000000'; // 0.3 ABT with 6 decimals
          if (params.amount === requiredAmount) {
            return {
              sufficient: false,
              token: { balance: '2500000' }, // 0.25 ABT
              reason: 'NO_ENOUGH_TOKEN',
            };
          }
          return {
            sufficient: true,
            token: { balance: '2500000' },
          };
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: false,
            reason: 'insufficient_balance',
            pending_amount: '200',
          })
        );
      });

      it('should return can_continue: false if balance exactly equals required amount minus 1 (boundary)', async () => {
        // Required: 0.3 ABT, Balance: 0.299999 ABT (insufficient by 1 unit)
        (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
          const requiredAmount = '3000000';
          if (params.amount === requiredAmount) {
            return {
              sufficient: false,
              token: { balance: '2999999' }, // 1 unit less than required
              reason: 'NO_ENOUGH_TOKEN',
            };
          }
          return {
            sufficient: true,
            token: { balance: '2999999' },
          };
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: false,
            reason: 'insufficient_balance',
            pending_amount: '200',
          })
        );
      });

      it('should return can_continue: true if balance exactly equals required amount (boundary)', async () => {
        // Required: 0.3 ABT, Balance: exactly 0.3 ABT (sufficient)
        (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
          const requiredAmount = '3000000';
          if (params.amount === requiredAmount) {
            return {
              sufficient: true,
              token: { balance: '3000000' }, // Exactly required amount
            };
          }
          return {
            sufficient: true,
            token: { balance: '3000000' },
          };
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
            payment_account_sufficient: true,
          })
        );
      });

      it('should return can_continue: false if balance can only pay off pending but not cover one more recharge', async () => {
        // Pending: 2 credits (need 2 recharges = 0.2 ABT)
        // Balance: 0.2 ABT (can pay off pending, but cannot cover one more recharge = 0.1 ABT)
        // Required: 0.2 + 0.1 = 0.3 ABT
        (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
          const requiredAmount = '3000000';
          if (params.amount === requiredAmount) {
            return {
              sufficient: false,
              token: { balance: '2000000' }, // Exactly enough to pay off pending, but not enough for one more
              reason: 'NO_ENOUGH_TOKEN',
            };
          }
          return {
            sufficient: true,
            token: { balance: '2000000' },
          };
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: false,
            reason: 'insufficient_balance',
            pending_amount: '200',
          })
        );
      });

      it('should return can_continue: true if balance is sufficient (greater than required)', async () => {
        // Required: 0.3 ABT, Balance: 0.5 ABT (sufficient)
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '5000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '5000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
            payment_account_sufficient: true,
          })
        );
      });

      it('should return can_continue: false if balance is zero', async () => {
        // Balance: 0 ABT
        (checkTokenBalance as jest.Mock).mockReset();
        (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
          const requiredAmount = '3000000';
          if (params.amount === requiredAmount) {
            return {
              sufficient: false,
              token: { balance: '0' },
              reason: 'NO_ENOUGH_TOKEN',
            };
          }
          return {
            sufficient: true,
            token: { balance: '0' },
          };
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: false,
            reason: 'insufficient_balance',
            pending_amount: '200',
          })
        );
      });
    });

    describe('Daily limit checks', () => {
      beforeEach(() => {
        // Pending: 2 credits, each recharge: 1 credit, need 2 times
        // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
      });

      it('should skip daily limit check if it is a new day', async () => {
        mockConfig.last_recharge_date = '2024-01-01';
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        // Mock current date to be different
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2024-01-02'));

        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should not check daily limits
        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'daily_limit_reached',
          })
        );

        jest.useRealTimers();
      });

      it('should return can_continue: false if attempt limit exceeded', async () => {
        // eslint-disable-next-line prefer-destructuring
        mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
        mockConfig.daily_stats = {
          attempt_count: 2,
          total_amount: '0',
        };
        mockConfig.daily_limits = {
          max_attempts: 3,
          max_amount: '0',
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);

        // Need 2 recharges, but only 1 attempt remaining (3 - 2 = 1)
        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'daily_limit_reached',
          detail: 'attempt_limit_exceeded',
        });
      });

      it('should return can_continue: false if amount limit exceeded', async () => {
        // eslint-disable-next-line prefer-destructuring
        mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
        mockConfig.daily_stats = {
          attempt_count: 0,
          total_amount: '1800000', // Already spent 1.8 ABT today
        };
        mockConfig.daily_limits = {
          max_attempts: 0,
          max_amount: '2000000', // Max 2 ABT per day
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);

        // Need 2 recharges = 0.2 ABT, but only 0.2 ABT remaining (2 - 1.8 = 0.2)
        // However, we need to check if the remaining amount is sufficient
        // Actually, if we need 2 recharges of 0.1 ABT each = 0.2 ABT, and we have 0.2 ABT remaining, it should pass
        // Let's set it so that we need more than what's remaining
        // Pending: 2 credits, need 2 recharges = 0.2 ABT, but only 0.15 ABT remaining
        mockConfig.daily_stats.total_amount = '1850000'; // Already spent 1.85 ABT, only 0.15 ABT remaining
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'daily_limit_reached',
          detail: 'amount_limit_exceeded',
        });
      });

      it('should pass daily limit check if limits are sufficient', async () => {
        // eslint-disable-next-line prefer-destructuring
        mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
        mockConfig.daily_stats = {
          attempt_count: 0,
          total_amount: '0',
        };
        mockConfig.daily_limits = {
          max_attempts: 5,
          max_amount: '50000000', // Max 50 ABT per day
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);

        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '20000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '20000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'daily_limit_reached',
          })
        );
      });
    });

    describe('Credit config checks', () => {
      it('should return can_continue: false if credit_config not found', async () => {
        mockConfig.price.metadata = {};
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'credit_config_not_found',
        });
      });

      it('should return can_continue: false if credit_amount not found', async () => {
        mockConfig.price.metadata = {
          credit_config: {},
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'credit_config_not_found',
        });
      });
    });

    describe('Payer checks', () => {
      it('should return can_continue: false if payer not found', async () => {
        jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: null } as any);
        mockConfig.payment_settings = {
          payment_method_options: {},
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'payer_not_found',
        });
      });

      it('should use customer.did as payer if not specified in payment_settings', async () => {
        jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({
          id: 'customer_123',
          did: 'did:customer:123',
        } as any);
        mockConfig.payment_settings = {
          payment_method_options: {},
        };
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '6000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(checkTokenBalance).toHaveBeenCalledWith(
          expect.objectContaining({
            userDid: 'did:customer:123',
          })
        );
      });
    });

    describe('Pending amount edge cases', () => {
      it('should handle pending amount exactly equals one recharge credit amount', async () => {
        // Pending: exactly 1 credit (one recharge), need 1 time
        // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });

      it('should handle pending amount exactly equals three recharge credit amount (boundary)', async () => {
        // Pending: exactly 3 credits (three recharges), need exactly 3 times
        // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).not.toHaveBeenCalledWith(
          expect.objectContaining({
            reason: 'too_many_recharges_required',
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });

      it('should handle pending amount one unit more than three recharge credit amount', async () => {
        // Pending: 4 credits (one unit more than 3), need 4 times
        // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(mockRes.json).toHaveBeenCalledWith({
          can_continue: false,
          reason: 'too_many_recharges_required',
          pending_amount: '400',
          required_recharge_times: '4',
          max_allowed_times: 3,
        });
      });

      it('should handle very small pending amount (1 credit)', async () => {
        // Pending: 1 credit, need 1 time
        // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format)
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
        (checkTokenBalance as jest.Mock)
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' }, // 10 ABT (1 * 5 + 5)
          })
          .mockResolvedValueOnce({
            sufficient: true,
            token: { balance: '10000000' },
          });

        await routeHandler(mockReq as Request, mockRes as Response);

        expect(checkTokenBalance).toHaveBeenCalledWith(
          expect.objectContaining({
            amount: '2000000', // 0.1 ABT (pay off 1 credit) + 0.1 ABT (one more recharge)
          })
        );
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });
    });

    describe('Pending amount from query parameter', () => {
      it('should use pending_amount from query if provided', async () => {
        mockReq.query = {
          customer_id: 'customer_123',
          currency_id: 'currency_123',
          pending_amount: '300', // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
        };

        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // Should use 300 from query, not 200 from getPendingAmounts
        // 3 credits / 1 credit per recharge = 3 times
        expect(checkTokenBalance).toHaveBeenCalled();
        expect(mockRes.json).toHaveBeenCalledWith(
          expect.objectContaining({
            can_continue: true,
          })
        );
      });
    });

    describe('Quantity handling', () => {
      it('should handle quantity > 1 correctly', async () => {
        mockConfig.quantity = 2; // Buy 2 units per recharge
        jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
        (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit (0.1 ABT) per unit
        jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);

        (checkTokenBalance as jest.Mock).mockResolvedValue({
          sufficient: true,
          token: { balance: '20000000' },
        });

        await routeHandler(mockReq as Request, mockRes as Response);

        // totalAmount should be 0.1 ABT * 2 = 0.2 ABT
        expect(checkTokenBalance).toHaveBeenCalledWith(
          expect.objectContaining({
            amount: '2000000', // 0.2 ABT (1 credit * 2 quantity)
          })
        );
      });
    });
  });

  describe('Error handling', () => {
    it('should handle errors and return 400', async () => {
      jest.spyOn(Customer, 'findByPkOrDid').mockRejectedValue(new Error('Database error'));

      await routeHandler(mockReq as Request, mockRes as Response);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: 'Database error',
      });
      expect(logger.error).toHaveBeenCalled();
    });
  });
});
