const fc = require('fast-check');
const { generateWithdrawalNo, MIN_WITHDRAWAL_AMOUNT } = require('../services/withdrawalService');

/**
 * Property-Based Tests for Withdrawal System
 * Feature: overseas-appointment-backend
 */

// Mock database to simulate storage
class MockDatabase {
  constructor() {
    this.users = new Map();
    this.withdrawals = new Map();
  }

  clear() {
    this.users.clear();
    this.withdrawals.clear();
  }

  createUser(userData) {
    const id = Math.random().toString(36).substring(7);
    const user = {
      id,
      balance: 0.00,
      status: 'active',
      createdAt: new Date(),
      ...userData,
    };
    this.users.set(id, user);
    return user;
  }

  findUserById(id) {
    return this.users.get(id);
  }

  updateUser(id, updateData) {
    const user = this.users.get(id);
    if (!user) return null;
    const updatedUser = { ...user, ...updateData };
    this.users.set(id, updatedUser);
    return updatedUser;
  }

  createWithdrawal(withdrawalData) {
    const id = Math.random().toString(36).substring(7);
    const withdrawal = {
      id,
      status: 'waiting',
      createdAt: new Date(),
      ...withdrawalData,
    };
    this.withdrawals.set(id, withdrawal);
    return withdrawal;
  }

  findWithdrawalByNo(withdrawalNo) {
    for (const withdrawal of this.withdrawals.values()) {
      if (withdrawal.withdrawalNo === withdrawalNo) {
        return withdrawal;
      }
    }
    return null;
  }

  findWithdrawalsByUser(userId) {
    const results = [];
    for (const withdrawal of this.withdrawals.values()) {
      if (withdrawal.userId === userId) {
        results.push(withdrawal);
      }
    }
    return results;
  }
}

const db = new MockDatabase();

// Mock withdrawal service functions
const mockWithdrawalService = {
  createWithdrawal: async (userId, withdrawalData) => {
    const { amount, paymentMethod, paymentDetails } = withdrawalData;

    const withdrawalAmount = parseFloat(amount);
    if (isNaN(withdrawalAmount) || withdrawalAmount <= 0) {
      throw new Error('Invalid withdrawal amount');
    }

    if (withdrawalAmount < MIN_WITHDRAWAL_AMOUNT) {
      throw new Error(`Minimum withdrawal amount is ${MIN_WITHDRAWAL_AMOUNT}`);
    }

    const validMethods = ['wechat', 'alipay', 'bank'];
    if (!validMethods.includes(paymentMethod)) {
      throw new Error('Invalid payment method');
    }

    if (!paymentDetails || typeof paymentDetails !== 'object' || Array.isArray(paymentDetails)) {
      throw new Error('Payment details are required');
    }

    const user = db.findUserById(userId);
    if (!user) {
      throw new Error('User not found');
    }

    const userBalance = parseFloat(user.balance);
    if (userBalance < withdrawalAmount) {
      throw new Error('Insufficient balance');
    }

    let withdrawalNo;
    let exists = true;
    
    while (exists) {
      withdrawalNo = generateWithdrawalNo();
      const existingWithdrawal = db.findWithdrawalByNo(withdrawalNo);
      exists = !!existingWithdrawal;
    }

    const withdrawal = db.createWithdrawal({
      userId,
      withdrawalNo,
      amount: withdrawalAmount,
      paymentMethod,
      paymentDetails,
      status: 'waiting',
    });

    return withdrawal;
  },
};

describe('Withdrawal System Property Tests', () => {
  beforeEach(() => {
    db.clear();
  });

  /**
   * Feature: overseas-appointment-backend, Property 20: Withdrawal amount validation
   * Validates: Requirements 5.1
   */
  describe('Property 20: Withdrawal amount validation', () => {
    test('should reject withdrawal when amount is below minimum threshold', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            balance: fc.float({ min: 100, max: 1000, noNaN: true }),
            withdrawalAmount: fc.float({ min: Math.fround(0.01), max: Math.fround(MIN_WITHDRAWAL_AMOUNT - 0.01), noNaN: true }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: data.balance,
            });

            await expect(
              mockWithdrawalService.createWithdrawal(user.id, {
                amount: data.withdrawalAmount,
                paymentMethod: 'wechat',
                paymentDetails: { account: 'test' },
              })
            ).rejects.toThrow(`Minimum withdrawal amount is ${MIN_WITHDRAWAL_AMOUNT}`);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should reject withdrawal when amount exceeds available balance', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            balance: fc.float({ min: Math.fround(MIN_WITHDRAWAL_AMOUNT), max: 500, noNaN: true }),
            excessAmount: fc.float({ min: Math.fround(0.01), max: 100, noNaN: true }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: data.balance,
            });

            const withdrawalAmount = data.balance + data.excessAmount;

            await expect(
              mockWithdrawalService.createWithdrawal(user.id, {
                amount: withdrawalAmount,
                paymentMethod: 'wechat',
                paymentDetails: { account: 'test' },
              })
            ).rejects.toThrow('Insufficient balance');
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should accept withdrawal when amount is valid and within balance', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            balance: fc.float({ min: 100, max: 1000, noNaN: true }),
            withdrawalRatio: fc.float({ min: Math.fround(0.1), max: Math.fround(1.0), noNaN: true }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: data.balance,
            });

            const withdrawalAmount = Math.max(
              MIN_WITHDRAWAL_AMOUNT,
              data.balance * data.withdrawalRatio
            );

            if (withdrawalAmount <= data.balance) {
              const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
                amount: withdrawalAmount,
                paymentMethod: 'wechat',
                paymentDetails: { account: 'test' },
              });

              expect(withdrawal).toBeDefined();
              expect(parseFloat(withdrawal.amount)).toBeCloseTo(withdrawalAmount, 2);
              expect(withdrawal.status).toBe('waiting');
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should reject invalid withdrawal amounts (negative, zero, NaN)', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.oneof(
            fc.constant(-10),
            fc.constant(0),
            fc.constant(-0.01),
            fc.float({ min: -1000, max: Math.fround(-0.01), noNaN: true })
          ),
          async (invalidAmount) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            await expect(
              mockWithdrawalService.createWithdrawal(user.id, {
                amount: invalidAmount,
                paymentMethod: 'wechat',
                paymentDetails: { account: 'test' },
              })
            ).rejects.toThrow('Invalid withdrawal amount');
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 21: Payment method support
   * Validates: Requirements 5.2
   */
  describe('Property 21: Payment method support', () => {
    test('should accept all valid payment methods (wechat, alipay, bank)', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            paymentMethod: fc.constantFrom('wechat', 'alipay', 'bank'),
            paymentDetails: fc.record({
              account: fc.string({ minLength: 5, maxLength: 50 }),
              name: fc.string({ minLength: 2, maxLength: 50 }),
            }),
            amount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 500, noNaN: true }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
              amount: data.amount,
              paymentMethod: data.paymentMethod,
              paymentDetails: data.paymentDetails,
            });

            expect(withdrawal).toBeDefined();
            expect(withdrawal.paymentMethod).toBe(data.paymentMethod);
            expect(withdrawal.paymentDetails).toEqual(data.paymentDetails);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should reject invalid payment methods', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 })
            .filter(s => !['wechat', 'alipay', 'bank'].includes(s)),
          async (invalidMethod) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            await expect(
              mockWithdrawalService.createWithdrawal(user.id, {
                amount: 50,
                paymentMethod: invalidMethod,
                paymentDetails: { account: 'test' },
              })
            ).rejects.toThrow('Invalid payment method');
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should store payment details correctly for each method', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            paymentMethod: fc.constantFrom('wechat', 'alipay', 'bank'),
            wechatDetails: fc.record({
              wechatId: fc.string({ minLength: 5, maxLength: 30 }),
              realName: fc.string({ minLength: 2, maxLength: 50 }),
            }),
            alipayDetails: fc.record({
              alipayAccount: fc.string({ minLength: 5, maxLength: 50 }),
              realName: fc.string({ minLength: 2, maxLength: 50 }),
            }),
            bankDetails: fc.record({
              bankName: fc.string({ minLength: 2, maxLength: 50 }),
              accountNumber: fc.string({ minLength: 10, maxLength: 30 }),
              accountName: fc.string({ minLength: 2, maxLength: 50 }),
            }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            let paymentDetails;
            if (data.paymentMethod === 'wechat') {
              paymentDetails = data.wechatDetails;
            } else if (data.paymentMethod === 'alipay') {
              paymentDetails = data.alipayDetails;
            } else {
              paymentDetails = data.bankDetails;
            }

            const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
              amount: 50,
              paymentMethod: data.paymentMethod,
              paymentDetails,
            });

            expect(withdrawal.paymentDetails).toEqual(paymentDetails);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should reject withdrawal when payment details are missing or invalid', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.oneof(
            fc.constant(null),
            fc.constant(undefined),
            fc.constant('string'),
            fc.constant(123),
            fc.constant(true),
            fc.constant([]),
            fc.constant(['item']),
          ),
          async (invalidDetails) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            await expect(
              mockWithdrawalService.createWithdrawal(user.id, {
                amount: 50,
                paymentMethod: 'wechat',
                paymentDetails: invalidDetails,
              })
            ).rejects.toThrow('Payment details are required');
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 22: Withdrawal initial state
   * Validates: Requirements 5.3
   */
  describe('Property 22: Withdrawal initial state', () => {
    test('should create withdrawal with status "waiting" and unique withdrawal number', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(
            fc.record({
              userId: fc.string({ minLength: 5, maxLength: 10 }),
              amount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 500, noNaN: true }),
              paymentMethod: fc.constantFrom('wechat', 'alipay', 'bank'),
            }),
            { minLength: 1, maxLength: 20 }
          ),
          async (withdrawalRequests) => {
            const users = [];
            for (const request of withdrawalRequests) {
              const user = db.createUser({
                wechatOpenId: request.userId,
                balance: 1000,
              });
              users.push(user);
            }

            const withdrawals = [];
            for (let i = 0; i < withdrawalRequests.length; i++) {
              const withdrawal = await mockWithdrawalService.createWithdrawal(users[i].id, {
                amount: withdrawalRequests[i].amount,
                paymentMethod: withdrawalRequests[i].paymentMethod,
                paymentDetails: { account: 'test' },
              });
              withdrawals.push(withdrawal);
            }

            // Check all withdrawals have status "waiting"
            for (const withdrawal of withdrawals) {
              expect(withdrawal.status).toBe('waiting');
            }

            // Check all withdrawal numbers are unique
            const withdrawalNos = withdrawals.map(w => w.withdrawalNo);
            const uniqueNos = new Set(withdrawalNos);
            expect(uniqueNos.size).toBe(withdrawalNos.length);

            // Check withdrawal number format (starts with "WD")
            for (const withdrawal of withdrawals) {
              expect(withdrawal.withdrawalNo).toMatch(/^WD\d+$/);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should initialize withdrawal with all required fields', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            amount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 500, noNaN: true }),
            paymentMethod: fc.constantFrom('wechat', 'alipay', 'bank'),
            paymentDetails: fc.record({
              account: fc.string({ minLength: 5, maxLength: 50 }),
              name: fc.string({ minLength: 2, maxLength: 50 }),
            }),
          }),
          async (data) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 1000,
            });

            const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, data);

            expect(withdrawal.id).toBeDefined();
            expect(withdrawal.userId).toBe(user.id);
            expect(withdrawal.withdrawalNo).toBeDefined();
            expect(withdrawal.withdrawalNo).toMatch(/^WD\d+$/);
            expect(parseFloat(withdrawal.amount)).toBeCloseTo(data.amount, 2);
            expect(withdrawal.paymentMethod).toBe(data.paymentMethod);
            expect(withdrawal.paymentDetails).toEqual(data.paymentDetails);
            expect(withdrawal.status).toBe('waiting');
            expect(withdrawal.createdAt).toBeDefined();
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should generate unique withdrawal numbers even with concurrent requests', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.integer({ min: 5, max: 50 }),
          async (numWithdrawals) => {
            const user = db.createUser({
              wechatOpenId: 'test-user',
              balance: 10000,
            });

            const promises = [];
            for (let i = 0; i < numWithdrawals; i++) {
              promises.push(
                mockWithdrawalService.createWithdrawal(user.id, {
                  amount: MIN_WITHDRAWAL_AMOUNT,
                  paymentMethod: 'wechat',
                  paymentDetails: { account: 'test' },
                })
              );
            }

            const withdrawals = await Promise.all(promises);

            const withdrawalNos = withdrawals.map(w => w.withdrawalNo);
            const uniqueNos = new Set(withdrawalNos);
            expect(uniqueNos.size).toBe(withdrawalNos.length);
          }
        ),
        { numRuns: 50 }
      );
    });
  });
});

// Mock admin withdrawal service functions
const mockAdminWithdrawalService = {
  approveWithdrawal: async (withdrawalId, adminId) => {
    const withdrawal = db.withdrawals.get(withdrawalId);
    if (!withdrawal) {
      throw new Error('Withdrawal not found');
    }

    if (withdrawal.status !== 'waiting') {
      throw new Error(`Cannot approve withdrawal with status: ${withdrawal.status}`);
    }

    const user = db.findUserById(withdrawal.userId);
    if (!user) {
      throw new Error('User not found');
    }

    const withdrawalAmount = parseFloat(withdrawal.amount);
    const userBalance = parseFloat(user.balance);

    if (userBalance < withdrawalAmount) {
      throw new Error('User has insufficient balance');
    }

    const newBalance = userBalance - withdrawalAmount;
    db.updateUser(user.id, { balance: newBalance });

    withdrawal.status = 'completed';
    withdrawal.reviewedBy = adminId;
    withdrawal.reviewedAt = new Date();
    withdrawal.completedAt = new Date();

    return {
      ...withdrawal,
      userBalance: newBalance,
    };
  },

  rejectWithdrawal: async (withdrawalId, adminId, reason) => {
    if (!reason || reason.trim() === '') {
      throw new Error('Rejection reason is required');
    }

    const withdrawal = db.withdrawals.get(withdrawalId);
    if (!withdrawal) {
      throw new Error('Withdrawal not found');
    }

    if (withdrawal.status !== 'waiting') {
      throw new Error(`Cannot reject withdrawal with status: ${withdrawal.status}`);
    }

    withdrawal.status = 'rejected';
    withdrawal.reviewedBy = adminId;
    withdrawal.reviewedAt = new Date();
    withdrawal.rejectionReason = reason;

    return withdrawal;
  },
};

/**
 * Feature: overseas-appointment-backend, Property 23: Admin withdrawal actions
 * Validates: Requirements 5.4
 */
describe('Property 23: Admin withdrawal actions', () => {
  beforeEach(() => {
    db.clear();
  });

  test('should allow admin to approve withdrawal in waiting status', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          balance: fc.float({ min: 100, max: 1000, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 100, noNaN: true }),
          adminId: fc.uuid(),
        }),
        async (data) => {
          // Create user with sufficient balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.balance,
          });

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: Math.min(data.withdrawalAmount, data.balance),
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          expect(withdrawal.status).toBe('waiting');

          // Admin approves withdrawal
          const approved = await mockAdminWithdrawalService.approveWithdrawal(
            withdrawal.id,
            data.adminId
          );

          expect(approved.status).toBe('completed');
          expect(approved.reviewedBy).toBe(data.adminId);
          expect(approved.reviewedAt).toBeDefined();
          expect(approved.completedAt).toBeDefined();
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should allow admin to reject withdrawal in waiting status', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          balance: fc.float({ min: 100, max: 1000, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 100, noNaN: true }),
          adminId: fc.uuid(),
          rejectionReason: fc.string({ minLength: 5, maxLength: 200 }),
        }),
        async (data) => {
          // Create user
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.balance,
          });

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: Math.min(data.withdrawalAmount, data.balance),
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          expect(withdrawal.status).toBe('waiting');

          // Admin rejects withdrawal
          const rejected = await mockAdminWithdrawalService.rejectWithdrawal(
            withdrawal.id,
            data.adminId,
            data.rejectionReason
          );

          expect(rejected.status).toBe('rejected');
          expect(rejected.reviewedBy).toBe(data.adminId);
          expect(rejected.reviewedAt).toBeDefined();
          expect(rejected.rejectionReason).toBe(data.rejectionReason);
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should reject approval of non-waiting withdrawals', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          status: fc.constantFrom('processing', 'completed', 'rejected'),
          adminId: fc.uuid(),
        }),
        async (data) => {
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: 1000,
          });

          // Create withdrawal and manually set status
          const withdrawal = db.createWithdrawal({
            userId: user.id,
            withdrawalNo: generateWithdrawalNo(),
            amount: 50,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
            status: data.status,
          });

          await expect(
            mockAdminWithdrawalService.approveWithdrawal(withdrawal.id, data.adminId)
          ).rejects.toThrow(`Cannot approve withdrawal with status: ${data.status}`);
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should reject rejection of non-waiting withdrawals', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          status: fc.constantFrom('processing', 'completed', 'rejected'),
          adminId: fc.uuid(),
          reason: fc.string({ minLength: 5, maxLength: 200 }),
        }),
        async (data) => {
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: 1000,
          });

          // Create withdrawal and manually set status
          const withdrawal = db.createWithdrawal({
            userId: user.id,
            withdrawalNo: generateWithdrawalNo(),
            amount: 50,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
            status: data.status,
          });

          await expect(
            mockAdminWithdrawalService.rejectWithdrawal(withdrawal.id, data.adminId, data.reason)
          ).rejects.toThrow(`Cannot reject withdrawal with status: ${data.status}`);
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should require rejection reason when rejecting withdrawal', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          adminId: fc.uuid(),
          invalidReason: fc.oneof(
            fc.constant(''),
            fc.constant('   '),
            fc.constant('\t\n'),
          ),
        }),
        async (data) => {
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: 1000,
          });

          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: 50,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          await expect(
            mockAdminWithdrawalService.rejectWithdrawal(
              withdrawal.id,
              data.adminId,
              data.invalidReason
            )
          ).rejects.toThrow('Rejection reason is required');
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should handle multiple admin actions on different withdrawals', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.array(
          fc.record({
            action: fc.constantFrom('approve', 'reject'),
            adminId: fc.uuid(),
            rejectionReason: fc.string({ minLength: 5, maxLength: 200 }),
          }),
          { minLength: 2, maxLength: 10 }
        ),
        async (actions) => {
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: 10000,
          });

          const withdrawals = [];
          for (let i = 0; i < actions.length; i++) {
            const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
              amount: MIN_WITHDRAWAL_AMOUNT,
              paymentMethod: 'wechat',
              paymentDetails: { account: 'test' },
            });
            withdrawals.push(withdrawal);
          }

          for (let i = 0; i < actions.length; i++) {
            const action = actions[i];
            const withdrawal = withdrawals[i];

            if (action.action === 'approve') {
              const result = await mockAdminWithdrawalService.approveWithdrawal(
                withdrawal.id,
                action.adminId
              );
              expect(result.status).toBe('completed');
              expect(result.reviewedBy).toBe(action.adminId);
            } else {
              const result = await mockAdminWithdrawalService.rejectWithdrawal(
                withdrawal.id,
                action.adminId,
                action.rejectionReason
              );
              expect(result.status).toBe('rejected');
              expect(result.reviewedBy).toBe(action.adminId);
              expect(result.rejectionReason).toBe(action.rejectionReason);
            }
          }
        }
      ),
      { numRuns: 50 }
    );
  });
});

/**
 * Feature: overseas-appointment-backend, Property 24: Withdrawal approval updates balance
 * Validates: Requirements 5.5
 */
describe('Property 24: Withdrawal approval updates balance', () => {
  beforeEach(() => {
    db.clear();
  });

  test('should decrease user balance by withdrawal amount when approved', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          initialBalance: fc.float({ min: 100, max: 1000, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 100, noNaN: true }),
          adminId: fc.uuid(),
        }),
        async (data) => {
          // Create user with initial balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.initialBalance,
          });

          const withdrawalAmount = Math.min(data.withdrawalAmount, data.initialBalance);

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: withdrawalAmount,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          const balanceBeforeApproval = parseFloat(user.balance);

          // Admin approves withdrawal
          const approved = await mockAdminWithdrawalService.approveWithdrawal(
            withdrawal.id,
            data.adminId
          );

          // Check balance was updated
          const updatedUser = db.findUserById(user.id);
          const balanceAfterApproval = parseFloat(updatedUser.balance);
          const expectedBalance = balanceBeforeApproval - withdrawalAmount;

          expect(balanceAfterApproval).toBeCloseTo(expectedBalance, 2);
          expect(approved.userBalance).toBeCloseTo(expectedBalance, 2);
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should not change user balance when withdrawal is rejected', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          initialBalance: fc.float({ min: 100, max: 1000, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 100, noNaN: true }),
          adminId: fc.uuid(),
          rejectionReason: fc.string({ minLength: 5, maxLength: 200 }),
        }),
        async (data) => {
          // Create user with initial balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.initialBalance,
          });

          const withdrawalAmount = Math.min(data.withdrawalAmount, data.initialBalance);

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: withdrawalAmount,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          const balanceBeforeRejection = parseFloat(user.balance);

          // Admin rejects withdrawal
          await mockAdminWithdrawalService.rejectWithdrawal(
            withdrawal.id,
            data.adminId,
            data.rejectionReason
          );

          // Check balance was NOT changed
          const updatedUser = db.findUserById(user.id);
          const balanceAfterRejection = parseFloat(updatedUser.balance);

          expect(balanceAfterRejection).toBeCloseTo(balanceBeforeRejection, 2);
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should handle multiple sequential approvals correctly', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          initialBalance: fc.float({ min: 500, max: 2000, noNaN: true }),
          numWithdrawals: fc.integer({ min: 2, max: 10 }),
          adminId: fc.uuid(),
        }),
        async (data) => {
          // Create user with initial balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.initialBalance,
          });

          let expectedBalance = data.initialBalance;
          const withdrawalAmount = MIN_WITHDRAWAL_AMOUNT;

          // Create and approve multiple withdrawals
          for (let i = 0; i < data.numWithdrawals; i++) {
            if (expectedBalance < withdrawalAmount) {
              break;
            }

            const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
              amount: withdrawalAmount,
              paymentMethod: 'wechat',
              paymentDetails: { account: 'test' },
            });

            await mockAdminWithdrawalService.approveWithdrawal(withdrawal.id, data.adminId);

            expectedBalance -= withdrawalAmount;

            const updatedUser = db.findUserById(user.id);
            expect(parseFloat(updatedUser.balance)).toBeCloseTo(expectedBalance, 2);
          }
        }
      ),
      { numRuns: 50 }
    );
  });

  test('should reject approval when user balance becomes insufficient', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          initialBalance: fc.float({ min: 50, max: 100, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 50, noNaN: true }),
          adminId: fc.uuid(),
        }),
        async (data) => {
          // Create user with initial balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.initialBalance,
          });

          const withdrawalAmount = Math.min(data.withdrawalAmount, data.initialBalance);

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: withdrawalAmount,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          // Manually reduce user balance to simulate concurrent operations
          db.updateUser(user.id, { balance: withdrawalAmount - 1 });

          // Admin tries to approve withdrawal
          await expect(
            mockAdminWithdrawalService.approveWithdrawal(withdrawal.id, data.adminId)
          ).rejects.toThrow('User has insufficient balance');

          // Verify withdrawal status is still waiting
          const unchangedWithdrawal = db.withdrawals.get(withdrawal.id);
          expect(unchangedWithdrawal.status).toBe('waiting');
        }
      ),
      { numRuns: 100 }
    );
  });

  test('should create transaction record when withdrawal is approved', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          initialBalance: fc.float({ min: 100, max: 1000, noNaN: true }),
          withdrawalAmount: fc.float({ min: MIN_WITHDRAWAL_AMOUNT, max: 100, noNaN: true }),
          adminId: fc.uuid(),
        }),
        async (data) => {
          // Create user with initial balance
          const user = db.createUser({
            wechatOpenId: 'test-user',
            balance: data.initialBalance,
          });

          const withdrawalAmount = Math.min(data.withdrawalAmount, data.initialBalance);

          // Create withdrawal
          const withdrawal = await mockWithdrawalService.createWithdrawal(user.id, {
            amount: withdrawalAmount,
            paymentMethod: 'wechat',
            paymentDetails: { account: 'test' },
          });

          // Admin approves withdrawal
          const approved = await mockAdminWithdrawalService.approveWithdrawal(
            withdrawal.id,
            data.adminId
          );

          // Verify transaction audit fields are set
          expect(approved.status).toBe('completed');
          expect(approved.reviewedBy).toBe(data.adminId);
          expect(approved.reviewedAt).toBeDefined();
          expect(approved.reviewedAt).toBeInstanceOf(Date);
          expect(approved.completedAt).toBeDefined();
          expect(approved.completedAt).toBeInstanceOf(Date);
        }
      ),
      { numRuns: 100 }
    );
  });
});
