/**
 * Payment Order Amount Statistics Property Tests
 * **Feature: payment-order-amount-statistics**
 */

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

// Mock database first
jest.mock('../config/database', () => ({
  sequelize: {
    define: jest.fn(() => ({})),
    transaction: jest.fn(() => Promise.resolve({
      commit: jest.fn(),
      rollback: jest.fn(),
    })),
    fn: jest.fn((fnName, ...args) => ({ fn: fnName, args })),
    col: jest.fn((colName) => ({ col: colName })),
  },
}));

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

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

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

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

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

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

const PaymentOrder = require('../models/PaymentOrder');
const Commission = require('../models/Commission');
const paymentOrderService = require('../services/paymentOrderService');

// Generator for payment order with various currency amounts
const paymentOrderArbitrary = () => fc.record({
  id: fc.uuid(),
  orderNo: fc.string({ minLength: 10, maxLength: 20 }).map(s => `PO${s}`),
  userId: fc.uuid(),
  amountRmb: fc.oneof(
    fc.constant(null),
    fc.constant(0),
    fc.double({ min: 0.01, max: 100000, noNaN: true })
  ),
  amountPeso: fc.oneof(
    fc.constant(null),
    fc.constant(0),
    fc.double({ min: 0.01, max: 100000, noNaN: true })
  ),
  amount: fc.option(fc.double({ min: 0.01, max: 100000, noNaN: true }), { nil: null }),
  serviceContent: fc.string({ minLength: 1, maxLength: 200 }),
  paymentTime: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
  status: fc.constantFrom('active', 'cancelled'),
  createdAt: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
});

// Helper to create mock order with associations
const createMockOrder = (order) => ({
  ...order,
  user: { id: order.userId, uid: '123456', nickname: 'Test User', phone: '1234567890' },
  appointment: null,
  creator: { id: 'admin-uuid', username: 'admin', email: 'admin@test.com' },
});

// Helper to calculate expected RMB sum (treating null/undefined as 0)
const calculateExpectedRmbSum = (orders) => {
  return orders.reduce((sum, order) => {
    const amount = order.amountRmb !== null && order.amountRmb !== undefined ? parseFloat(order.amountRmb) : 0;
    return sum + amount;
  }, 0);
};

// Helper to calculate expected Peso sum (treating null/undefined as 0)
const calculateExpectedPesoSum = (orders) => {
  return orders.reduce((sum, order) => {
    const amount = order.amountPeso !== null && order.amountPeso !== undefined ? parseFloat(order.amountPeso) : 0;
    return sum + amount;
  }, 0);
};

describe('Payment Order Amount Statistics - Property Tests', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });


  /**
   * **Feature: payment-order-amount-statistics, Property 1: RMB Sum Correctness**
   * *For any* set of payment orders matching the filter conditions, the returned totalRmb 
   * should equal the sum of all amountRmb values (treating null/undefined as 0) from those orders.
   * **Validates: Requirements 3.1, 3.4**
   */
  describe('Property 1: RMB Sum Correctness', () => {
    it('should return totalRmb equal to sum of all amountRmb values', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 0, maxLength: 20 }),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(orders);
            const expectedPesoSum = calculateExpectedPesoSum(orders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: totalRmb should equal the sum of all amountRmb values
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalRmb)).toBeCloseTo(expectedRmbSum, 2);
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should treat null amountRmb values as 0 in sum calculation', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(
            fc.record({
              id: fc.uuid(),
              orderNo: fc.string({ minLength: 10, maxLength: 20 }).map(s => `PO${s}`),
              userId: fc.uuid(),
              amountRmb: fc.constant(null), // All null values
              amountPeso: fc.oneof(fc.constant(null), fc.double({ min: 0.01, max: 100000, noNaN: true })),
              amount: fc.constant(null),
              serviceContent: fc.string({ minLength: 1, maxLength: 200 }),
              paymentTime: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
              status: fc.constantFrom('active', 'cancelled'),
              createdAt: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
            }),
            { minLength: 1, maxLength: 10 }
          ),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);

            // Setup mocks - all null RMB values should sum to 0
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: 0,
              totalPeso: calculateExpectedPesoSum(orders),
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: totalRmb should be "0.00" when all values are null
            expect(result.statistics.totalRmb).toBe('0.00');
          }
        ),
        { numRuns: 100 }
      );
    });
  });


  /**
   * **Feature: payment-order-amount-statistics, Property 2: Peso Sum Correctness**
   * *For any* set of payment orders matching the filter conditions, the returned totalPeso 
   * should equal the sum of all amountPeso values (treating null/undefined as 0) from those orders.
   * **Validates: Requirements 3.2, 3.4**
   */
  describe('Property 2: Peso Sum Correctness', () => {
    it('should return totalPeso equal to sum of all amountPeso values', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 0, maxLength: 20 }),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(orders);
            const expectedPesoSum = calculateExpectedPesoSum(orders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: totalPeso should equal the sum of all amountPeso values
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalPeso)).toBeCloseTo(expectedPesoSum, 2);
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should treat null amountPeso values as 0 in sum calculation', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(
            fc.record({
              id: fc.uuid(),
              orderNo: fc.string({ minLength: 10, maxLength: 20 }).map(s => `PO${s}`),
              userId: fc.uuid(),
              amountRmb: fc.oneof(fc.constant(null), fc.double({ min: 0.01, max: 100000, noNaN: true })),
              amountPeso: fc.constant(null), // All null values
              amount: fc.constant(null),
              serviceContent: fc.string({ minLength: 1, maxLength: 200 }),
              paymentTime: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
              status: fc.constantFrom('active', 'cancelled'),
              createdAt: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }),
            }),
            { minLength: 1, maxLength: 10 }
          ),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);

            // Setup mocks - all null Peso values should sum to 0
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: calculateExpectedRmbSum(orders),
              totalPeso: 0,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: totalPeso should be "0.00" when all values are null
            expect(result.statistics.totalPeso).toBe('0.00');
          }
        ),
        { numRuns: 100 }
      );
    });
  });


  /**
   * **Feature: payment-order-amount-statistics, Property 3: Filter Consistency**
   * *For any* combination of filters (currency, userId, status, dateRange), the statistics 
   * should only include amounts from orders that satisfy ALL applied filter conditions.
   * **Validates: Requirements 1.2, 3.3**
   */
  describe('Property 3: Filter Consistency', () => {
    it('should apply userId filter to statistics calculation', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 1, maxLength: 20 }),
          fc.uuid(),
          async (orders, filterUserId) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            // Filter orders that match the userId
            const filteredOrders = orders.filter(o => o.userId === filterUserId);
            const mockRows = filteredOrders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(filteredOrders);
            const expectedPesoSum = calculateExpectedPesoSum(filteredOrders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({ userId: filterUserId });

            // Assert: Statistics should reflect only filtered orders
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalRmb)).toBeCloseTo(expectedRmbSum, 2);
            expect(parseFloat(result.statistics.totalPeso)).toBeCloseTo(expectedPesoSum, 2);

            // Verify WHERE clause includes userId
            expect(PaymentOrder.findAndCountAll).toHaveBeenCalledTimes(1);
            const callArgs = PaymentOrder.findAndCountAll.mock.calls[0][0];
            expect(callArgs.where.userId).toBe(filterUserId);
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should apply status filter to statistics calculation', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 1, maxLength: 20 }),
          fc.constantFrom('active', 'cancelled'),
          async (orders, filterStatus) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            // Filter orders that match the status
            const filteredOrders = orders.filter(o => o.status === filterStatus);
            const mockRows = filteredOrders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(filteredOrders);
            const expectedPesoSum = calculateExpectedPesoSum(filteredOrders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({ status: filterStatus });

            // Assert: Statistics should reflect only filtered orders
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalRmb)).toBeCloseTo(expectedRmbSum, 2);
            expect(parseFloat(result.statistics.totalPeso)).toBeCloseTo(expectedPesoSum, 2);

            // Verify WHERE clause includes status
            expect(PaymentOrder.findAndCountAll).toHaveBeenCalledTimes(1);
            const callArgs = PaymentOrder.findAndCountAll.mock.calls[0][0];
            expect(callArgs.where.status).toBe(filterStatus);
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should apply currency filter to statistics calculation', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 1, maxLength: 20 }),
          fc.constantFrom('rmb', 'peso'),
          async (orders, currency) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            // Filter orders that match the currency filter
            const filteredOrders = orders.filter(o => {
              if (currency === 'rmb') {
                return o.amountRmb !== null && o.amountRmb > 0;
              } else {
                return o.amountPeso !== null && o.amountPeso > 0;
              }
            });
            const mockRows = filteredOrders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(filteredOrders);
            const expectedPesoSum = calculateExpectedPesoSum(filteredOrders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({ currency });

            // Assert: Statistics should reflect only filtered orders
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalRmb)).toBeCloseTo(expectedRmbSum, 2);
            expect(parseFloat(result.statistics.totalPeso)).toBeCloseTo(expectedPesoSum, 2);

            // Verify WHERE clause includes currency filter
            expect(PaymentOrder.findAndCountAll).toHaveBeenCalledTimes(1);
            const callArgs = PaymentOrder.findAndCountAll.mock.calls[0][0];
            if (currency === 'rmb') {
              expect(callArgs.where.amountRmb).toBeDefined();
            } else {
              expect(callArgs.where.amountPeso).toBeDefined();
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should apply multiple filters using AND logic to statistics', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 1, maxLength: 20 }),
          fc.constantFrom('active', 'cancelled'),
          fc.constantFrom('rmb', 'peso'),
          async (orders, filterStatus, currency) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            // Filter orders that match ALL conditions
            const filteredOrders = orders.filter(o => {
              const matchesStatus = o.status === filterStatus;
              const matchesCurrency = currency === 'rmb' 
                ? (o.amountRmb !== null && o.amountRmb > 0)
                : (o.amountPeso !== null && o.amountPeso > 0);
              return matchesStatus && matchesCurrency;
            });
            const mockRows = filteredOrders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(filteredOrders);
            const expectedPesoSum = calculateExpectedPesoSum(filteredOrders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({ 
              status: filterStatus, 
              currency 
            });

            // Assert: Statistics should reflect only orders matching ALL filters
            expect(result.statistics).toBeDefined();
            expect(parseFloat(result.statistics.totalRmb)).toBeCloseTo(expectedRmbSum, 2);
            expect(parseFloat(result.statistics.totalPeso)).toBeCloseTo(expectedPesoSum, 2);
          }
        ),
        { numRuns: 100 }
      );
    });
  });


  /**
   * **Feature: payment-order-amount-statistics, Property 4: Statistics Structure Consistency**
   * *For any* API response, the statistics object should always contain both totalRmb and 
   * totalPeso fields formatted as strings with two decimal places.
   * **Validates: Requirements 1.3, 2.3**
   */
  describe('Property 4: Statistics Structure Consistency', () => {
    it('should always return statistics with totalRmb and totalPeso as formatted strings', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 0, maxLength: 20 }),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);
            const expectedRmbSum = calculateExpectedRmbSum(orders);
            const expectedPesoSum = calculateExpectedPesoSum(orders);

            // Setup mocks
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: expectedRmbSum,
              totalPeso: expectedPesoSum,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: Statistics object structure
            expect(result.statistics).toBeDefined();
            expect(typeof result.statistics.totalRmb).toBe('string');
            expect(typeof result.statistics.totalPeso).toBe('string');
            
            // Assert: Two decimal places format
            expect(result.statistics.totalRmb).toMatch(/^\d+\.\d{2}$/);
            expect(result.statistics.totalPeso).toMatch(/^\d+\.\d{2}$/);
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should return "0.00" for empty result sets', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.constant([]), // Empty array
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();

            // Setup mocks for empty result
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: 0,
              rows: [],
            });
            PaymentOrder.findOne.mockResolvedValue({
              totalRmb: 0,
              totalPeso: 0,
            });
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: Should return "0.00" for both currencies
            expect(result.statistics.totalRmb).toBe('0.00');
            expect(result.statistics.totalPeso).toBe('0.00');
          }
        ),
        { numRuns: 100 }
      );
    });

    it('should handle null statistics result gracefully', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(paymentOrderArbitrary(), { minLength: 0, maxLength: 5 }),
          async (orders) => {
            // Clear mocks before each property test iteration
            jest.clearAllMocks();
            
            const mockRows = orders.map(createMockOrder);

            // Setup mocks - simulate null statistics result
            PaymentOrder.findAndCountAll.mockResolvedValue({
              count: mockRows.length,
              rows: mockRows,
            });
            PaymentOrder.findOne.mockResolvedValue(null);
            Commission.findAll.mockResolvedValue([]);

            // Act
            const result = await paymentOrderService.getPaymentOrders({});

            // Assert: Should still return valid statistics structure with "0.00"
            expect(result.statistics).toBeDefined();
            expect(result.statistics.totalRmb).toBe('0.00');
            expect(result.statistics.totalPeso).toBe('0.00');
          }
        ),
        { numRuns: 100 }
      );
    });
  });
});
