const fc = require('fast-check');
const { generateInvitationCode } = require('../services/authService');

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

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

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

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

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

  findUserByInvitationCode(code) {
    for (const user of this.users.values()) {
      if (user.invitationCode === code) {
        return user;
      }
    }
    return null;
  }

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

  createInvitation(invitationData) {
    const id = Math.random().toString(36).substring(7);
    const invitation = {
      id,
      rewardStatus: 'pending',
      createdAt: new Date(),
      ...invitationData,
    };
    this.invitations.set(id, invitation);
    return invitation;
  }

  findInvitationsByInviter(inviterId) {
    const results = [];
    for (const invitation of this.invitations.values()) {
      if (invitation.inviterId === inviterId) {
        results.push(invitation);
      }
    }
    return results;
  }

  findInvitationByInvitee(inviteeId) {
    for (const invitation of this.invitations.values()) {
      if (invitation.inviteeId === inviteeId) {
        return invitation;
      }
    }
    return null;
  }

  updateInvitation(id, updateData) {
    const invitation = this.invitations.get(id);
    if (!invitation) return null;
    const updated = { ...invitation, ...updateData };
    this.invitations.set(id, updated);
    return updated;
  }

  createAppointment(appointmentData) {
    const id = Math.random().toString(36).substring(7);
    const appointment = {
      id,
      status: 'pending',
      paidAt: null,
      createdAt: new Date(),
      ...appointmentData,
    };
    this.appointments.set(id, appointment);
    return appointment;
  }

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

const db = new MockDatabase();

// Mock invitation service functions
const mockInvitationService = {
  generateInvitationCodeForUser: async (userId) => {
    const user = db.findUserById(userId);
    if (!user) throw new Error('User not found');

    if (user.invitationCode) {
      return user;
    }

    let code;
    let exists = true;
    
    while (exists) {
      code = generateInvitationCode();
      const existingUser = db.findUserByInvitationCode(code);
      exists = !!existingUser;
    }

    return db.updateUser(userId, { invitationCode: code });
  },

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

    const invitations = db.findInvitationsByInviter(userId);
    const totalInvites = invitations.length;
    const paidInvites = invitations.filter(inv => inv.firstPaymentAt != null).length;
    const totalRewards = invitations
      .filter(inv => inv.rewardStatus === 'credited')
      .reduce((sum, inv) => sum + parseFloat(inv.rewardAmount || 0), 0);

    return {
      invitationCode: user.invitationCode,
      totalInvites,
      paidInvites,
      totalRewards: totalRewards.toFixed(2),
      availableBalance: parseFloat(user.balance).toFixed(2),
    };
  },

  recordInvitationRelationship: async (inviteeId, invitationCode) => {
    const inviter = db.findUserByInvitationCode(invitationCode);
    if (!inviter) throw new Error('Invalid invitation code');

    const existingInvitation = db.findInvitationByInvitee(inviteeId);
    if (existingInvitation) return existingInvitation;

    return db.createInvitation({
      inviterId: inviter.id,
      inviteeId,
      invitationCode,
      registeredAt: new Date(),
      rewardStatus: 'pending',
    });
  },

  processFirstPayment: async (userId, appointmentId) => {
    const invitation = db.findInvitationByInvitee(userId);
    if (!invitation) return null;
    if (invitation.firstPaymentAt) return null;

    const appointment = db.findAppointmentById(appointmentId);
    if (!appointment || !appointment.amount) {
      throw new Error('Invalid appointment or amount');
    }

    const rewardAmount = parseFloat(appointment.amount) * 0.1;

    const updatedInvitation = db.updateInvitation(invitation.id, {
      firstPaymentAt: new Date(),
      firstPaymentAmount: appointment.amount,
      rewardAmount,
      rewardStatus: 'credited',
    });

    const inviter = db.findUserById(invitation.inviterId);
    db.updateUser(inviter.id, {
      balance: parseFloat(inviter.balance) + rewardAmount,
    });

    return updatedInvitation;
  },
};

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

  /**
   * Feature: overseas-appointment-backend, Property 15: Invitation code uniqueness
   * Validates: Requirements 4.1
   */
  describe('Property 15: Invitation code uniqueness', () => {
    test('should generate unique invitation codes for multiple users', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(
            fc.record({
              wechatOpenId: fc.string({ minLength: 10, maxLength: 20 }),
              nickname: fc.string({ minLength: 1, maxLength: 50 }),
            }),
            { minLength: 2, maxLength: 10 }
          ),
          async (userDataArray) => {
            const users = [];
            for (const userData of userDataArray) {
              const user = db.createUser({
                wechatOpenId: userData.wechatOpenId,
                nickname: userData.nickname,
              });
              users.push(user);
            }

            const codes = [];
            for (const user of users) {
              const updatedUser = await mockInvitationService.generateInvitationCodeForUser(user.id);
              codes.push(updatedUser.invitationCode);
            }

            const uniqueCodes = new Set(codes);
            expect(uniqueCodes.size).toBe(codes.length);

            for (let i = 0; i < users.length; i++) {
              const user = db.findUserById(users[i].id);
              expect(user.invitationCode).toBe(codes[i]);
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should return existing code if user already has one', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 10, maxLength: 20 }),
          async (openId) => {
            const user = db.createUser({
              wechatOpenId: openId,
              nickname: 'Test User',
            });

            const result1 = await mockInvitationService.generateInvitationCodeForUser(user.id);
            const firstCode = result1.invitationCode;

            const result2 = await mockInvitationService.generateInvitationCodeForUser(user.id);
            const secondCode = result2.invitationCode;

            expect(firstCode).toBe(secondCode);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 16: Invitation relationship recording
   * Validates: Requirements 4.2
   */
  describe('Property 16: Invitation relationship recording', () => {
    test('should record invitation relationship when new user registers with valid code', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(
            fc.record({
              inviterOpenId: fc.string({ minLength: 10, maxLength: 20 }),
              inviteeOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            }),
            { minLength: 1, maxLength: 10 }
          ).filter(arr => {
            const openIds = arr.flatMap(item => [item.inviterOpenId, item.inviteeOpenId]);
            return new Set(openIds).size === openIds.length;
          }),
          async (invitationPairs) => {
            const inviters = [];
            for (const pair of invitationPairs) {
              const inviter = db.createUser({
                wechatOpenId: pair.inviterOpenId,
                nickname: 'Inviter',
              });
              const updatedInviter = await mockInvitationService.generateInvitationCodeForUser(inviter.id);
              inviters.push(updatedInviter);
            }

            for (let i = 0; i < invitationPairs.length; i++) {
              const invitee = db.createUser({
                wechatOpenId: invitationPairs[i].inviteeOpenId,
                nickname: 'Invitee',
                invitedBy: inviters[i].id,
              });

              const invitation = await mockInvitationService.recordInvitationRelationship(
                invitee.id,
                inviters[i].invitationCode
              );

              expect(invitation).toBeDefined();
              expect(invitation.inviterId).toBe(inviters[i].id);
              expect(invitation.inviteeId).toBe(invitee.id);
              expect(invitation.invitationCode).toBe(inviters[i].invitationCode);
              expect(invitation.registeredAt).toBeDefined();
              expect(invitation.rewardStatus).toBe('pending');
            }
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should not create duplicate invitation records for same invitee', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            inviterOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            inviteeOpenId: fc.string({ minLength: 10, maxLength: 20 }),
          }).filter(data => data.inviterOpenId !== data.inviteeOpenId),
          async (data) => {
            const inviter = db.createUser({
              wechatOpenId: data.inviterOpenId,
              nickname: 'Inviter',
            });
            const updatedInviter = await mockInvitationService.generateInvitationCodeForUser(inviter.id);

            const invitee = db.createUser({
              wechatOpenId: data.inviteeOpenId,
              nickname: 'Invitee',
              invitedBy: inviter.id,
            });

            const invitation1 = await mockInvitationService.recordInvitationRelationship(
              invitee.id,
              updatedInviter.invitationCode
            );

            const invitation2 = await mockInvitationService.recordInvitationRelationship(
              invitee.id,
              updatedInviter.invitationCode
            );

            expect(invitation1.id).toBe(invitation2.id);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 17: Reward calculation on first payment
   * Validates: Requirements 4.3
   */
  describe('Property 17: Reward calculation on first payment', () => {
    test('should calculate 10% reward and credit to inviter on first payment', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            inviterOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            inviteeOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            paymentAmount: fc.float({ min: 10, max: 1000, noNaN: true }),
          }).filter(data => data.inviterOpenId !== data.inviteeOpenId),
          async (data) => {
            const inviter = db.createUser({
              wechatOpenId: data.inviterOpenId,
              nickname: 'Inviter',
              balance: 0,
            });
            const updatedInviter = await mockInvitationService.generateInvitationCodeForUser(inviter.id);

            const invitee = db.createUser({
              wechatOpenId: data.inviteeOpenId,
              nickname: 'Invitee',
              invitedBy: inviter.id,
            });

            await mockInvitationService.recordInvitationRelationship(
              invitee.id,
              updatedInviter.invitationCode
            );

            const appointment = db.createAppointment({
              userId: invitee.id,
              amount: data.paymentAmount,
              status: 'pending',
            });

            const inviterBefore = db.findUserById(inviter.id);
            const balanceBefore = parseFloat(inviterBefore.balance);

            const invitation = await mockInvitationService.processFirstPayment(
              invitee.id,
              appointment.id
            );

            expect(invitation).toBeDefined();
            expect(invitation.firstPaymentAt).toBeDefined();
            expect(parseFloat(invitation.firstPaymentAmount)).toBeCloseTo(data.paymentAmount, 2);
            
            const expectedReward = data.paymentAmount * 0.1;
            expect(parseFloat(invitation.rewardAmount)).toBeCloseTo(expectedReward, 2);
            expect(invitation.rewardStatus).toBe('credited');

            const inviterAfter = db.findUserById(inviter.id);
            const balanceAfter = parseFloat(inviterAfter.balance);
            expect(balanceAfter).toBeCloseTo(balanceBefore + expectedReward, 2);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should not process reward for second payment', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            inviterOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            inviteeOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            firstPayment: fc.float({ min: 10, max: 500, noNaN: true }),
            secondPayment: fc.float({ min: 10, max: 500, noNaN: true }),
          }).filter(data => data.inviterOpenId !== data.inviteeOpenId),
          async (data) => {
            const inviter = db.createUser({
              wechatOpenId: data.inviterOpenId,
              nickname: 'Inviter',
              balance: 0,
            });
            const updatedInviter = await mockInvitationService.generateInvitationCodeForUser(inviter.id);

            const invitee = db.createUser({
              wechatOpenId: data.inviteeOpenId,
              nickname: 'Invitee',
              invitedBy: inviter.id,
            });

            await mockInvitationService.recordInvitationRelationship(
              invitee.id,
              updatedInviter.invitationCode
            );

            const appointment1 = db.createAppointment({
              userId: invitee.id,
              amount: data.firstPayment,
              status: 'pending',
            });

            await mockInvitationService.processFirstPayment(invitee.id, appointment1.id);

            const inviterAfterFirst = db.findUserById(inviter.id);
            const balanceAfterFirst = parseFloat(inviterAfterFirst.balance);

            const appointment2 = db.createAppointment({
              userId: invitee.id,
              amount: data.secondPayment,
              status: 'pending',
            });

            const result = await mockInvitationService.processFirstPayment(
              invitee.id,
              appointment2.id
            );

            expect(result).toBeNull();

            const inviterAfterSecond = db.findUserById(inviter.id);
            const balanceAfterSecond = parseFloat(inviterAfterSecond.balance);
            expect(balanceAfterSecond).toBeCloseTo(balanceAfterFirst, 2);
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 18: Invitation statistics accuracy
   * Validates: Requirements 4.4
   */
  describe('Property 18: Invitation statistics accuracy', () => {
    test('should accurately calculate invitation statistics', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            inviterOpenId: fc.string({ minLength: 10, maxLength: 20 }),
            invitees: fc.array(
              fc.record({
                openId: fc.string({ minLength: 10, maxLength: 20 }),
                hasPaid: fc.boolean(),
                paymentAmount: fc.float({ min: 10, max: 500, noNaN: true }),
              }),
              { minLength: 1, maxLength: 10 }
            ),
          }).filter(data => {
            const allIds = [data.inviterOpenId, ...data.invitees.map(i => i.openId)];
            return new Set(allIds).size === allIds.length;
          }),
          async (data) => {
            const inviter = db.createUser({
              wechatOpenId: data.inviterOpenId,
              nickname: 'Inviter',
              balance: 0,
            });
            const updatedInviter = await mockInvitationService.generateInvitationCodeForUser(inviter.id);

            let expectedTotalRewards = 0;
            let expectedPaidInvites = 0;

            for (const inviteeData of data.invitees) {
              const invitee = db.createUser({
                wechatOpenId: inviteeData.openId,
                nickname: 'Invitee',
                invitedBy: inviter.id,
              });

              await mockInvitationService.recordInvitationRelationship(
                invitee.id,
                updatedInviter.invitationCode
              );

              if (inviteeData.hasPaid) {
                const appointment = db.createAppointment({
                  userId: invitee.id,
                  amount: inviteeData.paymentAmount,
                  status: 'pending',
                });

                await mockInvitationService.processFirstPayment(invitee.id, appointment.id);
                
                expectedTotalRewards += inviteeData.paymentAmount * 0.1;
                expectedPaidInvites++;
              }
            }

            const stats = await mockInvitationService.getInvitationStats(inviter.id);

            expect(stats.invitationCode).toBe(updatedInviter.invitationCode);
            expect(stats.totalInvites).toBe(data.invitees.length);
            expect(stats.paidInvites).toBe(expectedPaidInvites);
            expect(parseFloat(stats.totalRewards)).toBeCloseTo(expectedTotalRewards, 2);
            expect(parseFloat(stats.availableBalance)).toBeCloseTo(expectedTotalRewards, 2);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('should return zero statistics for user with no invitations', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 10, maxLength: 20 }),
          async (openId) => {
            const user = db.createUser({
              wechatOpenId: openId,
              nickname: 'User',
            });

            const updatedUser = await mockInvitationService.generateInvitationCodeForUser(user.id);

            const stats = await mockInvitationService.getInvitationStats(user.id);

            expect(stats.invitationCode).toBe(updatedUser.invitationCode);
            expect(stats.totalInvites).toBe(0);
            expect(stats.paidInvites).toBe(0);
            expect(parseFloat(stats.totalRewards)).toBe(0);
            expect(parseFloat(stats.availableBalance)).toBe(0);
          }
        ),
        { numRuns: 100 }
      );
    });
  });
});
