const fc = require('fast-check');
const AppointmentService = require('../services/appointmentService');

/**
 * Property-Based Tests for Appointment System
 * Feature: overseas-appointment-backend
 * 
 * These tests verify appointment properties using property-based testing
 * to ensure correctness across a wide range of inputs.
 */

describe('Appointment Property Tests', () => {

  /**
   * Feature: overseas-appointment-backend, Property 10: Appointment validation
   * Validates: Requirements 3.1
   * 
   * For any appointment submission with missing or invalid required fields, 
   * the system should reject the request with specific validation errors.
   */
  describe('Property 10: Appointment validation', () => {
    test('Appointment data structure validation - required fields', () => {
      fc.assert(
        fc.property(
          fc.record({
            serviceId: fc.uuid(),
            realName: fc.string({ minLength: 1, maxLength: 100 }),
            contactMethod: fc.constantFrom('phone', 'whatsapp', 'wechat'),
            contactValue: fc.string({ minLength: 1, maxLength: 100 }),
            appointmentDate: fc.option(fc.date()),
            appointmentTime: fc.option(fc.string()),
            notes: fc.option(fc.string({ maxLength: 500 })),
            amount: fc.option(fc.double({ min: 0, max: 10000, noNaN: true })),
          }),
          (appointmentData) => {
            // Verify all required fields are present
            expect(appointmentData.serviceId).toBeDefined();
            expect(appointmentData.realName).toBeDefined();
            expect(appointmentData.contactMethod).toBeDefined();
            expect(appointmentData.contactValue).toBeDefined();

            // Verify serviceId is a valid UUID format
            expect(appointmentData.serviceId).toMatch(
              /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
            );

            // Verify realName is non-empty string
            expect(typeof appointmentData.realName).toBe('string');
            expect(appointmentData.realName.length).toBeGreaterThan(0);
            expect(appointmentData.realName.length).toBeLessThanOrEqual(100);

            // Verify contactMethod is valid enum value
            expect(['phone', 'whatsapp', 'wechat']).toContain(appointmentData.contactMethod);

            // Verify contactValue is non-empty string
            expect(typeof appointmentData.contactValue).toBe('string');
            expect(appointmentData.contactValue.length).toBeGreaterThan(0);
            expect(appointmentData.contactValue.length).toBeLessThanOrEqual(100);

            // Verify optional fields have correct types when present
            if (appointmentData.appointmentDate !== null) {
              expect(appointmentData.appointmentDate).toBeInstanceOf(Date);
            }
            if (appointmentData.notes !== null) {
              expect(typeof appointmentData.notes).toBe('string');
              expect(appointmentData.notes.length).toBeLessThanOrEqual(500);
            }
            if (appointmentData.amount !== null) {
              expect(typeof appointmentData.amount).toBe('number');
              expect(appointmentData.amount).toBeGreaterThanOrEqual(0);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Invalid appointment data is rejected - missing required fields', () => {
      fc.assert(
        fc.property(
          fc.record({
            serviceId: fc.option(fc.uuid(), { nil: undefined }),
            realName: fc.option(fc.string({ minLength: 1, maxLength: 100 }), { nil: undefined }),
            contactMethod: fc.option(fc.constantFrom('phone', 'whatsapp', 'wechat'), { nil: undefined }),
            contactValue: fc.option(fc.string({ minLength: 1, maxLength: 100 }), { nil: undefined }),
          }).filter(data => 
            // Ensure at least one required field is missing
            !data.serviceId || !data.realName || !data.contactMethod || !data.contactValue
          ),
          (invalidData) => {
            // Verify that at least one required field is missing
            const hasAllRequired = Boolean(
              invalidData.serviceId && 
              invalidData.realName && 
              invalidData.contactMethod && 
              invalidData.contactValue
            );
            
            expect(hasAllRequired).toBe(false);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Invalid contact methods are rejected', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter(
            s => !['phone', 'whatsapp', 'wechat'].includes(s)
          ),
          (invalidContactMethod) => {
            // Verify the contact method is not in the valid list
            expect(['phone', 'whatsapp', 'wechat']).not.toContain(invalidContactMethod);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Valid contact methods are accepted', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('phone', 'whatsapp', 'wechat'),
          (validContactMethod) => {
            // Verify the contact method is in the valid list
            expect(['phone', 'whatsapp', 'wechat']).toContain(validContactMethod);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 11: Appointment ID uniqueness
   * Validates: Requirements 3.2
   * 
   * For any set of created appointments, all appointment IDs should be unique 
   * across the system.
   */
  describe('Property 11: Appointment ID uniqueness', () => {
    test('Appointment number format is correct', () => {
      fc.assert(
        fc.property(
          fc.tuple(
            fc.date({ min: new Date('2020-01-01'), max: new Date('2099-12-31') }),
            fc.integer({ min: 100000, max: 999999 })
          ),
          ([date, randomNum]) => {
            // Simulate appointment number generation
            const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
            const appointmentNo = `APT${dateStr}${randomNum}`;
            
            // Verify format: APT + YYYYMMDD + 6-digit number
            expect(appointmentNo).toMatch(/^APT\d{8}\d{6}$/);
            expect(appointmentNo.length).toBe(17); // APT(3) + YYYYMMDD(8) + 6digits(6)
            
            // Verify prefix
            expect(appointmentNo.startsWith('APT')).toBe(true);
            
            // Extract and verify date component
            const dateComponent = appointmentNo.substring(3, 11);
            expect(dateComponent).toMatch(/^\d{8}$/);
            
            // Extract and verify random number component
            const randomComponent = appointmentNo.substring(11);
            expect(randomComponent).toMatch(/^\d{6}$/);
            expect(parseInt(randomComponent)).toBeGreaterThanOrEqual(100000);
            expect(parseInt(randomComponent)).toBeLessThanOrEqual(999999);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Appointment numbers with different random components are unique', () => {
      fc.assert(
        fc.property(
          fc.tuple(
            fc.date({ min: new Date('2020-01-01'), max: new Date('2099-12-31') }),
            fc.array(fc.integer({ min: 100000, max: 999999 }), { minLength: 2, maxLength: 100 })
          ),
          ([date, randomNums]) => {
            const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
            
            // Generate appointment numbers with the same date but different random numbers
            const appointmentNos = randomNums.map(num => `APT${dateStr}${num}`);
            
            // Verify uniqueness based on random component
            const uniqueNos = new Set(appointmentNos);
            const uniqueRandoms = new Set(randomNums);
            
            // Number of unique appointment numbers should equal number of unique random numbers
            expect(uniqueNos.size).toBe(uniqueRandoms.size);
          }
        ),
        { numRuns: 50 }
      );
    });

    test('Appointment number date component is valid', () => {
      fc.assert(
        fc.property(
          fc.date({ min: new Date('2020-01-01'), max: new Date('2099-12-31') }),
          (date) => {
            // Verify the date format used in appointment numbers
            const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
            
            // Should be 8 digits (YYYYMMDD)
            expect(dateStr).toMatch(/^\d{8}$/);
            expect(dateStr.length).toBe(8);
            
            // Verify year is reasonable
            const year = parseInt(dateStr.substring(0, 4));
            expect(year).toBeGreaterThanOrEqual(2020);
            expect(year).toBeLessThanOrEqual(2099);
            
            // Verify month is valid (01-12)
            const month = parseInt(dateStr.substring(4, 6));
            expect(month).toBeGreaterThanOrEqual(1);
            expect(month).toBeLessThanOrEqual(12);
            
            // Verify day is valid (01-31)
            const day = parseInt(dateStr.substring(6, 8));
            expect(day).toBeGreaterThanOrEqual(1);
            expect(day).toBeLessThanOrEqual(31);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Appointment numbers from different dates are distinguishable', () => {
      fc.assert(
        fc.property(
          fc.tuple(
            fc.date({ min: new Date('2020-01-01'), max: new Date('2099-12-31') }),
            fc.date({ min: new Date('2020-01-01'), max: new Date('2099-12-31') }),
            fc.integer({ min: 100000, max: 999999 })
          ).filter(([date1, date2]) => 
            date1.toISOString().slice(0, 10) !== date2.toISOString().slice(0, 10)
          ),
          ([date1, date2, randomNum]) => {
            const dateStr1 = date1.toISOString().slice(0, 10).replace(/-/g, '');
            const dateStr2 = date2.toISOString().slice(0, 10).replace(/-/g, '');
            
            const appointmentNo1 = `APT${dateStr1}${randomNum}`;
            const appointmentNo2 = `APT${dateStr2}${randomNum}`;
            
            // Even with same random number, different dates produce different appointment numbers
            expect(appointmentNo1).not.toBe(appointmentNo2);
          }
        ),
        { numRuns: 50 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 13: Valid status transitions
   * Validates: Requirements 3.4
   * 
   * For any appointment, status transitions should only occur through valid paths 
   * (e.g., pending→confirmed→in-progress→completed), and invalid transitions 
   * should be rejected.
   */
  describe('Property 13: Valid status transitions', () => {
    const validTransitions = {
      pending: ['confirmed', 'cancelled'],
      confirmed: ['in-progress', 'cancelled'],
      'in-progress': ['completed', 'cancelled'],
      completed: [],
      cancelled: [],
    };

    test('Valid status transitions are accepted', () => {
      fc.assert(
        fc.property(
          fc.constantFrom(
            ['pending', 'confirmed'],
            ['pending', 'cancelled'],
            ['confirmed', 'in-progress'],
            ['confirmed', 'cancelled'],
            ['in-progress', 'completed'],
            ['in-progress', 'cancelled']
          ),
          ([currentStatus, newStatus]) => {
            // Verify the transition is valid
            const isValid = AppointmentService.isValidStatusTransition(currentStatus, newStatus);
            expect(isValid).toBe(true);
            
            // Verify the new status is in the allowed transitions list
            expect(validTransitions[currentStatus]).toContain(newStatus);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Invalid status transitions are rejected', () => {
      fc.assert(
        fc.property(
          fc.constantFrom(
            ['pending', 'in-progress'],
            ['pending', 'completed'],
            ['confirmed', 'pending'],
            ['confirmed', 'completed'],
            ['in-progress', 'pending'],
            ['in-progress', 'confirmed'],
            ['completed', 'pending'],
            ['completed', 'confirmed'],
            ['completed', 'in-progress'],
            ['completed', 'cancelled'],
            ['cancelled', 'pending'],
            ['cancelled', 'confirmed'],
            ['cancelled', 'in-progress'],
            ['cancelled', 'completed']
          ),
          ([currentStatus, newStatus]) => {
            // Verify the transition is invalid
            const isValid = AppointmentService.isValidStatusTransition(currentStatus, newStatus);
            expect(isValid).toBe(false);
            
            // Verify the new status is NOT in the allowed transitions list
            expect(validTransitions[currentStatus]).not.toContain(newStatus);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Terminal states do not allow transitions', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('completed', 'cancelled'),
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          (terminalStatus, newStatus) => {
            // Terminal states should not allow any transitions
            const isValid = AppointmentService.isValidStatusTransition(terminalStatus, newStatus);
            expect(isValid).toBe(false);
            
            // Verify terminal states have empty transition lists
            expect(validTransitions[terminalStatus]).toEqual([]);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Status transition paths are acyclic', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('pending', 'confirmed', 'in-progress'),
          (status) => {
            // Build a path from current status following valid transitions
            const visited = new Set([status]);
            let currentStatus = status;
            
            // Follow valid transitions
            while (validTransitions[currentStatus].length > 0) {
              const nextStatus = validTransitions[currentStatus][0];
              
              // Verify we don't revisit a status (no cycles)
              expect(visited.has(nextStatus)).toBe(false);
              
              visited.add(nextStatus);
              currentStatus = nextStatus;
            }
            
            // Verify we end at a terminal state
            expect(['completed', 'cancelled']).toContain(currentStatus);
          }
        ),
        { numRuns: 50 }
      );
    });

    test('All statuses have defined transition rules', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          (status) => {
            // Verify every status has a defined transition rule
            expect(validTransitions).toHaveProperty(status);
            expect(Array.isArray(validTransitions[status])).toBe(true);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Transition validation is consistent with defined rules', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          (currentStatus, newStatus) => {
            const isValid = AppointmentService.isValidStatusTransition(currentStatus, newStatus);
            const isInTransitionList = validTransitions[currentStatus].includes(newStatus);
            
            // Validation result should match whether status is in transition list
            expect(isValid).toBe(isInTransitionList);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 14: Appointment query filtering
   * Validates: Requirements 3.5
   * 
   * For any status filter and pagination parameters, querying appointments 
   * should return only appointments matching the status with correct pagination.
   */
  describe('Property 14: Appointment query filtering', () => {
    test('Pagination parameters are validated correctly', () => {
      fc.assert(
        fc.property(
          fc.record({
            page: fc.integer({ min: 1, max: 1000 }),
            limit: fc.integer({ min: 1, max: 100 }),
          }),
          (paginationParams) => {
            // Verify page is positive
            expect(paginationParams.page).toBeGreaterThanOrEqual(1);
            
            // Verify limit is within acceptable range
            expect(paginationParams.limit).toBeGreaterThanOrEqual(1);
            expect(paginationParams.limit).toBeLessThanOrEqual(100);
            
            // Calculate offset
            const offset = (paginationParams.page - 1) * paginationParams.limit;
            expect(offset).toBeGreaterThanOrEqual(0);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Status filter accepts valid appointment statuses', () => {
      fc.assert(
        fc.property(
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          (status) => {
            // Verify status is a valid appointment status
            const validStatuses = ['pending', 'confirmed', 'in-progress', 'completed', 'cancelled'];
            expect(validStatuses).toContain(status);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Pagination calculation is correct', () => {
      fc.assert(
        fc.property(
          fc.record({
            page: fc.integer({ min: 1, max: 100 }),
            limit: fc.integer({ min: 1, max: 100 }),
            totalCount: fc.integer({ min: 0, max: 10000 }),
          }),
          ({ page, limit, totalCount }) => {
            // Calculate offset
            const offset = (page - 1) * limit;
            
            // Verify offset calculation
            expect(offset).toBe((page - 1) * limit);
            expect(offset).toBeGreaterThanOrEqual(0);
            
            // Calculate total pages
            const totalPages = Math.ceil(totalCount / limit);
            
            // Verify total pages calculation
            if (totalCount === 0) {
              expect(totalPages).toBe(0);
            } else {
              expect(totalPages).toBeGreaterThan(0);
              expect(totalPages).toBeGreaterThanOrEqual(Math.floor(totalCount / limit));
              expect(totalPages).toBeLessThanOrEqual(Math.floor(totalCount / limit) + 1);
            }
            
            // Verify page is within valid range or beyond
            if (page <= totalPages) {
              expect(page).toBeLessThanOrEqual(totalPages);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Query options structure is valid', () => {
      fc.assert(
        fc.property(
          fc.record({
            page: fc.option(fc.integer({ min: 1, max: 1000 })),
            limit: fc.option(fc.integer({ min: 1, max: 100 })),
            status: fc.option(fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled')),
          }),
          (queryOptions) => {
            // Verify optional fields have correct types when present
            if (queryOptions.page !== null) {
              expect(typeof queryOptions.page).toBe('number');
              expect(queryOptions.page).toBeGreaterThanOrEqual(1);
            }
            
            if (queryOptions.limit !== null) {
              expect(typeof queryOptions.limit).toBe('number');
              expect(queryOptions.limit).toBeGreaterThanOrEqual(1);
              expect(queryOptions.limit).toBeLessThanOrEqual(100);
            }
            
            if (queryOptions.status !== null) {
              expect(typeof queryOptions.status).toBe('string');
              expect(['pending', 'confirmed', 'in-progress', 'completed', 'cancelled']).toContain(queryOptions.status);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Pagination metadata is consistent', () => {
      fc.assert(
        fc.property(
          fc.record({
            page: fc.integer({ min: 1, max: 100 }),
            limit: fc.integer({ min: 1, max: 100 }),
            total: fc.integer({ min: 0, max: 10000 }),
          }),
          ({ page, limit, total }) => {
            const totalPages = Math.ceil(total / limit);
            const offset = (page - 1) * limit;
            
            // Create pagination metadata
            const pagination = {
              page,
              limit,
              total,
              totalPages,
            };
            
            // Verify pagination metadata consistency
            expect(pagination.page).toBe(page);
            expect(pagination.limit).toBe(limit);
            expect(pagination.total).toBe(total);
            expect(pagination.totalPages).toBe(totalPages);
            
            // Verify relationships
            expect(pagination.totalPages * pagination.limit).toBeGreaterThanOrEqual(pagination.total);
            if (pagination.totalPages > 0) {
              expect((pagination.totalPages - 1) * pagination.limit).toBeLessThan(pagination.total);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Filter combinations are valid', () => {
      fc.assert(
        fc.property(
          fc.record({
            status: fc.option(fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled')),
            page: fc.integer({ min: 1, max: 100 }),
            limit: fc.integer({ min: 1, max: 100 }),
          }),
          (filterOptions) => {
            // Verify filter options can be combined
            const hasStatusFilter = filterOptions.status !== null;
            const hasPagination = filterOptions.page && filterOptions.limit;
            
            // Both filters and pagination should be valid independently
            if (hasStatusFilter) {
              expect(['pending', 'confirmed', 'in-progress', 'completed', 'cancelled']).toContain(filterOptions.status);
            }
            
            if (hasPagination) {
              expect(filterOptions.page).toBeGreaterThanOrEqual(1);
              expect(filterOptions.limit).toBeGreaterThanOrEqual(1);
              expect(filterOptions.limit).toBeLessThanOrEqual(100);
            }
          }
        ),
        { numRuns: 100 }
      );
    });
  });
});
