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

/**
 * Property-Based Tests for Admin Appointment Management
 * Feature: overseas-appointment-backend
 * 
 * These tests verify admin appointment management properties using property-based testing
 * with mock data to ensure correctness across a wide range of inputs.
 */

// Mock appointment database to simulate storage
class MockAppointmentDatabase {
  constructor() {
    this.appointments = new Map();
    this.users = new Map();
    this.services = new Map();
  }

  clear() {
    this.appointments.clear();
    this.users.clear();
    this.services.clear();
  }

  createUser(userData) {
    const id = Math.random().toString(36).substring(7);
    const user = {
      id,
      nickname: userData.nickname || `User ${id}`,
      realName: userData.realName || null,
      phone: userData.phone || null,
      wechatId: userData.wechatId || null,
      ...userData,
    };
    this.users.set(id, user);
    return user;
  }

  createService(serviceData) {
    const id = Math.random().toString(36).substring(7);
    const service = {
      id,
      titleZh: serviceData.titleZh || `Service ${id}`,
      categoryId: serviceData.categoryId || 'cat-1',
      ...serviceData,
    };
    this.services.set(id, service);
    return service;
  }

  createAppointment(appointmentData) {
    const id = Math.random().toString(36).substring(7);
    const appointment = {
      id,
      appointmentNo: `APT${Date.now()}${Math.floor(Math.random() * 1000)}`,
      userId: appointmentData.userId,
      serviceId: appointmentData.serviceId,
      realName: appointmentData.realName || 'Test User',
      contactMethod: appointmentData.contactMethod || 'phone',
      contactValue: appointmentData.contactValue || '1234567890',
      status: appointmentData.status || 'pending',
      amount: appointmentData.amount || 100.00,
      createdAt: appointmentData.createdAt || new Date(),
      ...appointmentData,
    };
    this.appointments.set(id, appointment);
    return appointment;
  }

  findAppointments(options = {}) {
    let appointments = Array.from(this.appointments.values());

    // Apply filters
    if (options.where) {
      appointments = appointments.filter((appointment) => {
        for (const [key, value] of Object.entries(options.where)) {
          // Handle $or at the top level
          if (key === '$or' && Array.isArray(value)) {
            const matches = value.some((condition) => {
              for (const [field, fieldCondition] of Object.entries(condition)) {
                if (fieldCondition.$like) {
                  const searchTerm = fieldCondition.$like.replace(/%/g, '');
                  if (!appointment[field]) return false;
                  const appointmentValue = String(appointment[field]);
                  return appointmentValue.includes(searchTerm);
                }
                if (fieldCondition.$in && Array.isArray(fieldCondition.$in)) {
                  return fieldCondition.$in.includes(appointment[field]);
                }
              }
              return false;
            });
            if (!matches) return false;
          } else if (key === 'createdAt' && typeof value === 'object') {
            // Handle date range
            const appointmentDate = new Date(appointment.createdAt);
            if (value.$gte && appointmentDate < new Date(value.$gte)) {
              return false;
            }
            if (value.$lt && appointmentDate >= new Date(value.$lt)) {
              return false;
            }
          } else if (appointment[key] !== value) {
            return false;
          }
        }
        return true;
      });
    }

    // Apply pagination
    const total = appointments.length;
    if (options.offset !== undefined) {
      appointments = appointments.slice(options.offset);
    }
    if (options.limit !== undefined) {
      appointments = appointments.slice(0, options.limit);
    }

    return { appointments, total };
  }

  findAppointmentById(id) {
    return this.appointments.get(id);
  }

  updateAppointment(id, updateData) {
    const appointment = this.appointments.get(id);
    if (!appointment) return null;

    const updatedAppointment = {
      ...appointment,
      ...updateData,
      updatedAt: new Date(),
    };
    this.appointments.set(id, updatedAppointment);
    return updatedAppointment;
  }

  getStatistics(options = {}) {
    let appointments = Array.from(this.appointments.values());

    // Apply date filter
    if (options.startDate || options.endDate) {
      appointments = appointments.filter((appointment) => {
        const appointmentDate = new Date(appointment.createdAt);
        if (options.startDate && appointmentDate < new Date(options.startDate)) {
          return false;
        }
        if (options.endDate) {
          const endDate = new Date(options.endDate);
          endDate.setDate(endDate.getDate() + 1);
          if (appointmentDate >= endDate) {
            return false;
          }
        }
        return true;
      });
    }

    // Apply category filter
    if (options.categoryId) {
      appointments = appointments.filter((appointment) => {
        const service = this.services.get(appointment.serviceId);
        return service && service.categoryId === options.categoryId;
      });
    }

    const totalAppointments = appointments.length;

    // Count by status
    const statusCounts = {
      pending: 0,
      confirmed: 0,
      'in-progress': 0,
      completed: 0,
      cancelled: 0,
    };

    appointments.forEach((appointment) => {
      if (statusCounts.hasOwnProperty(appointment.status)) {
        statusCounts[appointment.status]++;
      }
    });

    // Calculate completion rate
    const completedCount = statusCounts.completed;
    const totalNonCancelled = totalAppointments - statusCounts.cancelled;
    const completionRate = totalNonCancelled > 0 ? (completedCount / totalNonCancelled) * 100 : 0;

    // Calculate total revenue
    const totalRevenue = appointments
      .filter((a) => a.status === 'completed')
      .reduce((sum, a) => sum + (a.amount || 0), 0);

    return {
      totalAppointments,
      statusCounts,
      completionRate: parseFloat(completionRate.toFixed(2)),
      totalRevenue,
    };
  }
}

describe('Admin Appointment Management Property Tests', () => {
  let mockDb;

  beforeEach(() => {
    mockDb = new MockAppointmentDatabase();
  });

  /**
   * **Feature: overseas-appointment-backend, Property 35: Appointment search and filter**
   * **Validates: Requirements 8.1**
   * 
   * For any search criteria (user, service, date range), the admin appointment list 
   * should return matching appointments.
   */
  describe('Property 35: Appointment search and filter', () => {
    test('should return appointments matching search by appointment number', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 5, maxLength: 15 }
          ),
          fc.string({ minLength: 3, maxLength: 8 }).filter(s => s.trim().length > 0 && !s.includes('%')),
          (appointmentsData, searchTerm) => {
            mockDb.clear();

            // Create users and services
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments, some with search term in appointment number
            const appointments = appointmentsData.map((data, index) => {
              const appointmentNo = index % 3 === 0 
                ? `APT${searchTerm}${index}` 
                : `APT${Date.now()}${index}`;
              
              return mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
                appointmentNo,
              });
            });

            // Search by appointment number
            const result = mockDb.findAppointments({
              where: {
                $or: [
                  { appointmentNo: { $like: `%${searchTerm}%` } },
                ],
              },
            });

            // All returned appointments should match the search criteria
            result.appointments.forEach((appointment) => {
              expect(appointment.appointmentNo).toContain(searchTerm);
            });

            // Count should match expected
            const expectedCount = appointments.filter(a => 
              a.appointmentNo.includes(searchTerm)
            ).length;
            expect(result.total).toBe(expectedCount);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should filter appointments by status', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 10, maxLength: 20 }
          ),
          fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
          (appointmentsData, filterStatus) => {
            mockDb.clear();

            // Create users and services
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments
            appointmentsData.forEach((data) => {
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Query with status filter
            const result = mockDb.findAppointments({
              where: { status: filterStatus },
            });

            // All returned appointments should have the filtered status
            result.appointments.forEach((appointment) => {
              expect(appointment.status).toBe(filterStatus);
            });

            // Count should match
            const expectedCount = appointmentsData.filter(
              (a) => a.status === filterStatus
            ).length;
            expect(result.total).toBe(expectedCount);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should filter appointments by date range', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 5, max: 15 }), // number of appointments
          fc.date({ min: new Date('2024-01-01'), max: new Date('2024-12-31') }), // start date
          fc.integer({ min: 1, max: 30 }), // days range
          (numAppointments, startDate, daysRange) => {
            mockDb.clear();

            // Create users and services
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Calculate end date
            const endDate = new Date(startDate);
            endDate.setDate(endDate.getDate() + daysRange);

            // Create appointments with various dates
            const appointments = [];
            for (let i = 0; i < numAppointments; i++) {
              const appointmentDate = new Date(startDate);
              appointmentDate.setDate(appointmentDate.getDate() + Math.floor(Math.random() * (daysRange + 10)));
              
              appointments.push(mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'pending',
                createdAt: appointmentDate,
              }));
            }

            // Query with date range filter
            const result = mockDb.findAppointments({
              where: {
                createdAt: {
                  $gte: startDate,
                  $lt: new Date(endDate.getTime() + 24 * 60 * 60 * 1000), // Add one day
                },
              },
            });

            // All returned appointments should be within the date range
            result.appointments.forEach((appointment) => {
              const appointmentDate = new Date(appointment.createdAt);
              expect(appointmentDate >= startDate).toBe(true);
              expect(appointmentDate < new Date(endDate.getTime() + 24 * 60 * 60 * 1000)).toBe(true);
            });

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should filter appointments by service', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'completed'),
            }),
            { minLength: 10, maxLength: 20 }
          ),
          fc.integer({ min: 2, max: 5 }), // number of services
          (appointmentsData, numServices) => {
            mockDb.clear();

            // Create user
            const user = mockDb.createUser({ nickname: 'Test User' });

            // Create multiple services
            const services = Array.from({ length: numServices }, (_, i) =>
              mockDb.createService({ titleZh: `Service ${i}` })
            );

            // Create appointments distributed across services
            appointmentsData.forEach((data, index) => {
              const service = services[index % services.length];
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Pick a random service to filter by
            const filterService = services[0];

            // Query with service filter
            const result = mockDb.findAppointments({
              where: { serviceId: filterService.id },
            });

            // All returned appointments should have the filtered service
            result.appointments.forEach((appointment) => {
              expect(appointment.serviceId).toBe(filterService.id);
            });

            // Count should match
            const expectedCount = appointmentsData.filter(
              (_, index) => index % services.length === 0
            ).length;
            expect(result.total).toBe(expectedCount);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should respect pagination limits', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 10, max: 30 }), // total appointments
          fc.integer({ min: 1, max: 10 }), // page size
          fc.integer({ min: 1, max: 5 }), // page number
          (totalAppointments, pageSize, pageNumber) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments
            for (let i = 0; i < totalAppointments; i++) {
              mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'pending',
              });
            }

            // Calculate offset
            const offset = (pageNumber - 1) * pageSize;

            // Query with pagination
            const result = mockDb.findAppointments({
              where: {},
              limit: pageSize,
              offset: offset,
            });

            // Should not exceed page size
            expect(result.appointments.length).toBeLessThanOrEqual(pageSize);

            // Total should match
            expect(result.total).toBe(totalAppointments);

            // Calculate expected page count
            const totalPages = Math.ceil(totalAppointments / pageSize);

            // If not the last page, should return exactly pageSize items
            if (pageNumber < totalPages) {
              expect(result.appointments.length).toBe(pageSize);
            }

            // If on the last page, should return remaining items
            if (pageNumber === totalPages) {
              const expectedRemaining = totalAppointments - (pageNumber - 1) * pageSize;
              expect(result.appointments.length).toBe(expectedRemaining);
            }

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * **Feature: overseas-appointment-backend, Property 39: Statistical report accuracy**
   * **Validates: Requirements 8.5**
   * 
   * For any date range, appointment statistics (volume, completion rate) should 
   * match the actual appointment data.
   */
  describe('Property 39: Statistical report accuracy', () => {
    test('should calculate total appointments correctly', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 5, maxLength: 20 }
          ),
          (appointmentsData) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments
            appointmentsData.forEach((data) => {
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Get statistics
            const stats = mockDb.getStatistics({});

            // Total should match
            expect(stats.totalAppointments).toBe(appointmentsData.length);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should count appointments by status correctly', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'in-progress', 'completed', 'cancelled'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 10, maxLength: 25 }
          ),
          (appointmentsData) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments
            appointmentsData.forEach((data) => {
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Get statistics
            const stats = mockDb.getStatistics({});

            // Count expected status counts
            const expectedCounts = {
              pending: 0,
              confirmed: 0,
              'in-progress': 0,
              completed: 0,
              cancelled: 0,
            };

            appointmentsData.forEach((data) => {
              expectedCounts[data.status]++;
            });

            // Verify status counts match
            expect(stats.statusCounts.pending).toBe(expectedCounts.pending);
            expect(stats.statusCounts.confirmed).toBe(expectedCounts.confirmed);
            expect(stats.statusCounts['in-progress']).toBe(expectedCounts['in-progress']);
            expect(stats.statusCounts.completed).toBe(expectedCounts.completed);
            expect(stats.statusCounts.cancelled).toBe(expectedCounts.cancelled);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should calculate completion rate correctly', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 10 }), // completed
          fc.integer({ min: 0, max: 10 }), // pending
          fc.integer({ min: 0, max: 5 }), // cancelled
          (completedCount, pendingCount, cancelledCount) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create completed appointments
            for (let i = 0; i < completedCount; i++) {
              mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'completed',
                amount: 100,
              });
            }

            // Create pending appointments
            for (let i = 0; i < pendingCount; i++) {
              mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'pending',
                amount: 100,
              });
            }

            // Create cancelled appointments
            for (let i = 0; i < cancelledCount; i++) {
              mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'cancelled',
                amount: 100,
              });
            }

            // Get statistics
            const stats = mockDb.getStatistics({});

            // Calculate expected completion rate
            const totalNonCancelled = completedCount + pendingCount;
            const expectedRate = totalNonCancelled > 0 
              ? parseFloat(((completedCount / totalNonCancelled) * 100).toFixed(2))
              : 0;

            // Verify completion rate
            expect(stats.completionRate).toBe(expectedRate);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should calculate total revenue correctly', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'confirmed', 'completed', 'cancelled'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 5, maxLength: 15 }
          ),
          (appointmentsData) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Create appointments
            appointmentsData.forEach((data) => {
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Get statistics
            const stats = mockDb.getStatistics({});

            // Calculate expected revenue (only completed appointments)
            const expectedRevenue = appointmentsData
              .filter((a) => a.status === 'completed')
              .reduce((sum, a) => sum + a.amount, 0);

            // Verify revenue matches (with small tolerance for floating point)
            expect(Math.abs(stats.totalRevenue - expectedRevenue)).toBeLessThan(0.01);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should filter statistics by date range', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 5, max: 15 }), // number of appointments
          fc.date({ min: new Date('2024-01-01'), max: new Date('2024-06-30') }), // start date
          fc.integer({ min: 10, max: 30 }), // days range
          (numAppointments, startDate, daysRange) => {
            mockDb.clear();

            // Create user and service
            const user = mockDb.createUser({ nickname: 'Test User' });
            const service = mockDb.createService({ titleZh: 'Test Service' });

            // Calculate end date
            const endDate = new Date(startDate);
            endDate.setDate(endDate.getDate() + daysRange);

            // Create appointments with various dates
            const appointmentsInRange = [];
            for (let i = 0; i < numAppointments; i++) {
              const appointmentDate = new Date(startDate);
              const daysOffset = Math.floor(Math.random() * (daysRange + 20)) - 5; // Some before, some after
              appointmentDate.setDate(appointmentDate.getDate() + daysOffset);
              
              const appointment = mockDb.createAppointment({
                userId: user.id,
                serviceId: service.id,
                realName: `User ${i}`,
                status: 'completed',
                amount: 100,
                createdAt: appointmentDate,
              });

              // Track which appointments are in range
              if (appointmentDate >= startDate && appointmentDate < new Date(endDate.getTime() + 24 * 60 * 60 * 1000)) {
                appointmentsInRange.push(appointment);
              }
            }

            // Get statistics with date filter
            const stats = mockDb.getStatistics({
              startDate: startDate.toISOString().split('T')[0],
              endDate: endDate.toISOString().split('T')[0],
            });

            // Total should match appointments in range
            expect(stats.totalAppointments).toBe(appointmentsInRange.length);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should filter statistics by category', () => {
      fc.assert(
        fc.property(
          fc.array(
            fc.record({
              realName: fc.string({ minLength: 3, maxLength: 20 }),
              status: fc.constantFrom('pending', 'completed'),
              amount: fc.float({ min: 10, max: 1000, noNaN: true }),
            }),
            { minLength: 10, maxLength: 20 }
          ),
          fc.integer({ min: 2, max: 4 }), // number of categories
          (appointmentsData, numCategories) => {
            mockDb.clear();

            // Create user
            const user = mockDb.createUser({ nickname: 'Test User' });

            // Create categories and services
            const categories = Array.from({ length: numCategories }, (_, i) => `cat-${i}`);
            const services = categories.map((catId) =>
              mockDb.createService({ titleZh: `Service ${catId}`, categoryId: catId })
            );

            // Create appointments distributed across services
            appointmentsData.forEach((data, index) => {
              const service = services[index % services.length];
              mockDb.createAppointment({
                ...data,
                userId: user.id,
                serviceId: service.id,
              });
            });

            // Pick a category to filter by
            const filterCategory = categories[0];

            // Get statistics with category filter
            const stats = mockDb.getStatistics({
              categoryId: filterCategory,
            });

            // Count expected appointments in this category
            const expectedCount = appointmentsData.filter(
              (_, index) => index % services.length === 0
            ).length;

            // Total should match
            expect(stats.totalAppointments).toBe(expectedCount);

            return true;
          }
        ),
        { numRuns: 100 }
      );
    });
  });
});
