/**
 * Commission Service Property Tests
 * Tests for commission calculation correctness
 */

const fc = require('fast-check');

// Mock database first
const mockTransaction = {
  commit: jest.fn(),
  rollback: jest.fn(),
};

jest.mock('../config/database', () => ({
  sequelize: {
    define: jest.fn(() => ({})),
    transaction: jest.fn(() => Promise.resolve(mockTransaction)),
  },
}));

// Mock models
jest.mock('../models/User', () => ({
  findByPk: jest.fn(),
  count: jest.fn(),
  findAll: jest.fn(),
}));

jest.mock('../models/Commission', () => ({
  create: jest.fn(),
  findAll: jest.fn(),
  findAndCountAll: jest.fn(),
  findOne: jest.fn(),
  count: jest.fn(),
  sum: jest.fn(),
}));

jest.mock('../models/PaymentOrder', () => ({
  findByPk: jest.fn(),
}));

jest.mock('../services/commissionConfigService', () => ({
  getCommissionRate: jest.fn(),
}));

const User = require('../models/User');
const Commission = require('../models/Commission');
const PaymentOrder = require('../models/PaymentOrder');
const { sequelize } = require('../config/database');
const commissionConfigService = require('../services/commissionConfigService');
const commissionService = require('../services/commissionService');

describe('Commission Service - Property Tests', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockTransaction.commit.mockClear();
    mockTransaction.rollback.mockClear();
    sequelize.transaction.mockResolvedValue(mockTransaction);
  });

  /**
   * **Feature: invitation-commission-system, Property 2: Commission calculation correctness**
   * *For any* payment order for an invited user, the commission amount should equal
   * paymentAmount × currentCommissionRate.
   * **Validates: Requirements 2.1**
   */
  describe('Property 2: Commission calculation correctness', () => {
    it('should calculate commission as paymentAmount × commissionRate', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.double({ min: 0.01, max: 100000, noNaN: true }), // payment amount
          fc.double({ min: 0.001, max: 0.5, noNaN: true }), // commission rate
          async (paymentAmount, commissionRate) => {
            // Arrange
            const inviterId = 'inviter-uuid';
            const inviteeId = 'invitee-uuid';
            const paymentOrderId = 'order-uuid';

            PaymentOrder.findByPk.mockResolvedValue({
              id: paymentOrderId,
              amount: paymentAmount,
              status: 'active',
              user: {
                id: inviteeId,
                invitedBy: inviterId,
              },
            });

            commissionConfigService.getCommissionRate.mockResolvedValue(commissionRate);

            let createdCommission = null;
            Commission.create.mockImplementation((data) => {
              createdCommission = { ...data, id: 'commission-uuid' };
              return Promise.resolve(createdCommission);
            });

            User.findByPk.mockResolvedValue({
              id: inviterId,
              balance: 0,
              save: jest.fn(),
            });

            // Act
            await commissionService.calculateCommission(paymentOrderId);

            // Assert: Commission amount should equal paymentAmount × commissionRate
            const expectedCommission = paymentAmount * commissionRate;
            expect(createdCommission.commissionAmount).toBeCloseTo(expectedCommission, 8);
            expect(createdCommission.paymentAmount).toBeCloseTo(paymentAmount, 8);
            expect(createdCommission.commissionRate).toBeCloseTo(commissionRate, 8);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * **Feature: invitation-commission-system, Property 3: Balance update on commission**
   * *For any* payment order that generates commission, the inviter's balance should
   * increase by exactly the commission amount.
   * **Validates: Requirements 2.2**
   */
  describe('Property 3: Balance update on commission', () => {
    it('should increase inviter balance by commission amount', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.double({ min: 0.01, max: 100000, noNaN: true }), // payment amount
          fc.double({ min: 0, max: 10000, noNaN: true }), // initial balance
          async (paymentAmount, initialBalance) => {
            // Arrange
            const commissionRate = 0.02;
            const inviterId = 'inviter-uuid';
            const inviteeId = 'invitee-uuid';
            const paymentOrderId = 'order-uuid';

            PaymentOrder.findByPk.mockResolvedValue({
              id: paymentOrderId,
              amount: paymentAmount,
              status: 'active',
              user: {
                id: inviteeId,
                invitedBy: inviterId,
              },
            });

            commissionConfigService.getCommissionRate.mockResolvedValue(commissionRate);

            Commission.create.mockResolvedValue({
              id: 'commission-uuid',
              commissionAmount: paymentAmount * commissionRate,
            });

            let updatedBalance = null;
            const mockInviter = {
              id: inviterId,
              balance: initialBalance,
              save: jest.fn().mockImplementation(function() {
                updatedBalance = this.balance;
                return Promise.resolve();
              }),
            };
            User.findByPk.mockResolvedValue(mockInviter);

            // Act
            await commissionService.calculateCommission(paymentOrderId);

            // Assert: Balance should increase by commission amount
            const expectedBalance = initialBalance + (paymentAmount * commissionRate);
            expect(updatedBalance).toBeCloseTo(expectedBalance, 8);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * **Feature: invitation-commission-system, Property 5: No commission for non-invited users**
   * *For any* payment order where the user has no inviter (invitedBy is null),
   * no commission record should be created.
   * **Validates: Requirements 2.4**
   */
  describe('Property 5: No commission for non-invited users', () => {
    it('should not create commission when user has no inviter', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.double({ min: 0.01, max: 100000, noNaN: true }), // payment amount
          async (paymentAmount) => {
            // Arrange: User without inviter
            const paymentOrderId = 'order-uuid';

            PaymentOrder.findByPk.mockResolvedValue({
              id: paymentOrderId,
              amount: paymentAmount,
              status: 'active',
              user: {
                id: 'user-uuid',
                invitedBy: null, // No inviter
              },
            });

            Commission.create.mockClear();

            // Act
            const result = await commissionService.calculateCommission(paymentOrderId);

            // Assert: No commission should be created
            expect(result).toBeNull();
            expect(Commission.create).not.toHaveBeenCalled();
          }
        ),
        { numRuns: 100 }
      );
    });
  });
});

/**
 * **Feature: invitation-commission-system, Property 4: Multiple payments generate multiple commissions**
 * *For any* invited user with N payment orders, there should be exactly N commission records.
 * **Validates: Requirements 2.3**
 */
describe('Property 4: Multiple payments generate multiple commissions', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockTransaction.commit.mockClear();
    mockTransaction.rollback.mockClear();
    sequelize.transaction.mockResolvedValue(mockTransaction);
  });

  it('each payment should generate a separate commission', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.array(fc.double({ min: 0.01, max: 10000, noNaN: true }), { minLength: 1, maxLength: 5 }),
        async (paymentAmounts) => {
          // Arrange
          const inviterId = 'inviter-uuid';
          const inviteeId = 'invitee-uuid';
          const commissionRate = 0.02;
          
          let commissionsCreated = 0;
          
          // For each payment, simulate commission creation
          for (let i = 0; i < paymentAmounts.length; i++) {
            const paymentOrderId = `order-uuid-${i}`;
            const paymentAmount = paymentAmounts[i];
            
            PaymentOrder.findByPk.mockResolvedValue({
              id: paymentOrderId,
              amount: paymentAmount,
              status: 'active',
              user: {
                id: inviteeId,
                invitedBy: inviterId,
              },
            });

            commissionConfigService.getCommissionRate.mockResolvedValue(commissionRate);

            Commission.create.mockImplementation((data) => {
              commissionsCreated++;
              return Promise.resolve({ ...data, id: `commission-uuid-${i}` });
            });

            User.findByPk.mockResolvedValue({
              id: inviterId,
              balance: 0,
              save: jest.fn(),
            });

            // Act
            await commissionService.calculateCommission(paymentOrderId);
          }

          // Assert: Number of commissions should equal number of payments
          expect(commissionsCreated).toBe(paymentAmounts.length);
        }
      ),
      { numRuns: 50 }
    );
  });
});

/**
 * **Feature: invitation-commission-system, Property 6: Commission rate change isolation**
 * *For any* commission record, changing the global commission rate should not affect
 * the recorded commission amount. Each commission stores its own rate at creation time.
 * **Validates: Requirements 3.3, 3.4**
 */
describe('Property 6: Commission rate change isolation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockTransaction.commit.mockClear();
    mockTransaction.rollback.mockClear();
    sequelize.transaction.mockResolvedValue(mockTransaction);
  });

  it('should store commission rate at creation time, unaffected by later rate changes', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.double({ min: 0.01, max: 100000, noNaN: true }), // payment amount
        fc.double({ min: 0.001, max: 0.5, noNaN: true }), // initial commission rate
        fc.double({ min: 0.001, max: 0.5, noNaN: true }), // new commission rate (after change)
        async (paymentAmount, initialRate, newRate) => {
          // Arrange
          const inviterId = 'inviter-uuid';
          const inviteeId = 'invitee-uuid';
          const paymentOrderId = 'order-uuid';

          PaymentOrder.findByPk.mockResolvedValue({
            id: paymentOrderId,
            amount: paymentAmount,
            status: 'active',
            user: {
              id: inviteeId,
              invitedBy: inviterId,
            },
          });

          // Set initial commission rate
          commissionConfigService.getCommissionRate.mockResolvedValue(initialRate);

          let createdCommission = null;
          Commission.create.mockImplementation((data) => {
            createdCommission = { ...data, id: 'commission-uuid' };
            return Promise.resolve(createdCommission);
          });

          User.findByPk.mockResolvedValue({
            id: inviterId,
            balance: 0,
            save: jest.fn(),
          });

          // Act: Create commission with initial rate
          await commissionService.calculateCommission(paymentOrderId);

          // Store the commission values at creation time
          const originalCommissionRate = createdCommission.commissionRate;
          const originalCommissionAmount = createdCommission.commissionAmount;

          // Simulate rate change (this would happen via commissionConfigService.setCommissionRate)
          commissionConfigService.getCommissionRate.mockResolvedValue(newRate);

          // Assert: The stored commission should still have the original rate and amount
          // (The commission record is immutable once created)
          expect(originalCommissionRate).toBeCloseTo(initialRate, 8);
          expect(originalCommissionAmount).toBeCloseTo(paymentAmount * initialRate, 8);
          
          // The stored rate should NOT equal the new rate (unless they happen to be the same)
          // This verifies that the commission stores its own rate, not a reference to global config
          if (Math.abs(initialRate - newRate) > 0.0001) {
            expect(originalCommissionRate).not.toBeCloseTo(newRate, 4);
          }
        }
      ),
      { numRuns: 100 }
    );
  });

  it('should create new commissions with new rate after rate change', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.double({ min: 0.01, max: 10000, noNaN: true }), // payment amount 1
        fc.double({ min: 0.01, max: 10000, noNaN: true }), // payment amount 2
        fc.double({ min: 0.01, max: 0.1, noNaN: true }), // initial rate
        fc.double({ min: 0.01, max: 0.1, noNaN: true }), // new rate
        async (paymentAmount1, paymentAmount2, initialRate, newRate) => {
          // Skip if rates are too similar
          if (Math.abs(initialRate - newRate) < 0.001) return;

          // Arrange
          const inviterId = 'inviter-uuid';
          const inviteeId = 'invitee-uuid';

          const createdCommissions = [];

          Commission.create.mockImplementation((data) => {
            const commission = { ...data, id: `commission-uuid-${createdCommissions.length}` };
            createdCommissions.push(commission);
            return Promise.resolve(commission);
          });

          User.findByPk.mockResolvedValue({
            id: inviterId,
            balance: 0,
            save: jest.fn(),
          });

          // First payment with initial rate
          PaymentOrder.findByPk.mockResolvedValue({
            id: 'order-uuid-1',
            amount: paymentAmount1,
            status: 'active',
            user: { id: inviteeId, invitedBy: inviterId },
          });
          commissionConfigService.getCommissionRate.mockResolvedValue(initialRate);
          await commissionService.calculateCommission('order-uuid-1');

          // Second payment with new rate (simulating rate change)
          PaymentOrder.findByPk.mockResolvedValue({
            id: 'order-uuid-2',
            amount: paymentAmount2,
            status: 'active',
            user: { id: inviteeId, invitedBy: inviterId },
          });
          commissionConfigService.getCommissionRate.mockResolvedValue(newRate);
          await commissionService.calculateCommission('order-uuid-2');

          // Assert: Each commission should have its own rate
          expect(createdCommissions.length).toBe(2);
          expect(createdCommissions[0].commissionRate).toBeCloseTo(initialRate, 8);
          expect(createdCommissions[1].commissionRate).toBeCloseTo(newRate, 8);
          
          // Commission amounts should be calculated with their respective rates
          expect(createdCommissions[0].commissionAmount).toBeCloseTo(paymentAmount1 * initialRate, 8);
          expect(createdCommissions[1].commissionAmount).toBeCloseTo(paymentAmount2 * newRate, 8);
        }
      ),
      { numRuns: 50 }
    );
  });
});
