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

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

// Mock user database to simulate storage
class MockUserDatabase {
  constructor() {
    this.users = new Map();
  }

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

  create(userData) {
    const id = Math.random().toString(36).substring(7);
    const user = {
      id,
      language: 'zh',
      balance: 0.00,
      status: 'active',
      createdAt: new Date(),
      updatedAt: new Date(),
      ...userData,
    };
    this.users.set(id, user);
    return user;
  }

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

  update(id, updateData) {
    const user = this.users.get(id);
    if (!user) return null;
    
    const updatedUser = {
      ...user,
      ...updateData,
      updatedAt: new Date(),
    };
    this.users.set(id, updatedUser);
    return updatedUser;
  }
}

// Mock user service functions
const mockUserService = {
  db: new MockUserDatabase(),

  getUserProfile: async (userId) => {
    const user = mockUserService.db.findById(userId);
    if (!user) {
      const error = new Error('User not found');
      error.statusCode = 404;
      error.code = 'USER_NOT_FOUND';
      throw error;
    }
    return user;
  },

  setUserLanguage: async (userId, language) => {
    const validLanguages = ['zh', 'en', 'pt'];
    if (!validLanguages.includes(language)) {
      const error = new Error('Invalid language code. Must be one of: zh, en, pt');
      error.statusCode = 400;
      error.code = 'INVALID_LANGUAGE';
      throw error;
    }

    const user = mockUserService.db.findById(userId);
    if (!user) {
      const error = new Error('User not found');
      error.statusCode = 404;
      error.code = 'USER_NOT_FOUND';
      throw error;
    }

    return mockUserService.db.update(userId, { language });
  },

  updateUserProfile: async (userId, updateData) => {
    const user = mockUserService.db.findById(userId);
    if (!user) {
      const error = new Error('User not found');
      error.statusCode = 404;
      error.code = 'USER_NOT_FOUND';
      throw error;
    }

    // Only allow updating specific fields
    const allowedFields = ['nickname', 'avatar', 'realName', 'phone', 'whatsapp', 'wechatId'];
    const filteredData = {};
    
    allowedFields.forEach(field => {
      if (updateData[field] !== undefined) {
        filteredData[field] = updateData[field];
      }
    });

    // Validate phone format if provided
    if (filteredData.phone && filteredData.phone.length > 0) {
      const phoneRegex = /^[+]?[\d\s-()]+$/;
      if (!phoneRegex.test(filteredData.phone)) {
        const error = new Error('Invalid phone number format');
        error.statusCode = 400;
        error.code = 'INVALID_PHONE';
        throw error;
      }
    }

    // Validate whatsapp format if provided
    if (filteredData.whatsapp && filteredData.whatsapp.length > 0) {
      const whatsappRegex = /^[+]?[\d\s-()]+$/;
      if (!whatsappRegex.test(filteredData.whatsapp)) {
        const error = new Error('Invalid WhatsApp number format');
        error.statusCode = 400;
        error.code = 'INVALID_WHATSAPP';
        throw error;
      }
    }

    return mockUserService.db.update(userId, filteredData);
  },
};

describe('User Management Property Tests', () => {
  beforeEach(() => {
    mockUserService.db.clear();
  });

  /**
   * Feature: overseas-appointment-backend, Property 4: Language preference persistence
   * Validates: Requirements 1.4
   * 
   * For any user and supported language (zh, en, pt), setting the language preference 
   * should persist and be reflected in subsequent API responses.
   */
  describe('Property 4: Language preference persistence', () => {
    test('Setting language preference persists and is retrievable', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
            language: fc.constantFrom('zh', 'en', 'pt'),
          }),
          async (userData) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
              language: 'zh', // Start with default language
            });

            // Set language preference
            const updatedUser = await mockUserService.setUserLanguage(user.id, userData.language);

            // Verify the language was updated in the response
            expect(updatedUser.language).toBe(userData.language);

            // Retrieve user profile to verify persistence
            const profile = await mockUserService.getUserProfile(user.id);

            // Verify language persisted in database
            expect(profile.language).toBe(userData.language);

            // Verify the language is one of the supported languages
            expect(['zh', 'en', 'pt']).toContain(profile.language);
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Language preference changes are reflected immediately', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.array(fc.constantFrom('zh', 'en', 'pt'), { minLength: 2, maxLength: 5 }),
          async (userData, languageSequence) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
              language: 'zh',
            });

            // Apply language changes in sequence
            for (const language of languageSequence) {
              await mockUserService.setUserLanguage(user.id, language);
              
              // Verify each change is immediately reflected
              const profile = await mockUserService.getUserProfile(user.id);
              expect(profile.language).toBe(language);
            }

            // Final verification
            const finalProfile = await mockUserService.getUserProfile(user.id);
            expect(finalProfile.language).toBe(languageSequence[languageSequence.length - 1]);
          }
        ),
        { numRuns: 50 }
      );
    });

    test('Invalid language codes are rejected', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.string({ minLength: 1, maxLength: 10 }).filter(
            lang => !['zh', 'en', 'pt'].includes(lang)
          ),
          async (userData, invalidLanguage) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
              language: 'zh',
            });

            // Attempt to set invalid language should throw
            await expect(
              mockUserService.setUserLanguage(user.id, invalidLanguage)
            ).rejects.toThrow('Invalid language code');

            // Verify language remains unchanged
            const profile = await mockUserService.getUserProfile(user.id);
            expect(profile.language).toBe('zh');
          }
        ),
        { numRuns: 50 }
      );
    });
  });

  /**
   * Feature: overseas-appointment-backend, Property 5: Profile update round-trip
   * Validates: Requirements 1.5
   * 
   * For any valid profile update data, updating a user's profile and then 
   * retrieving it should return the updated values.
   */
  describe('Property 5: Profile update round-trip', () => {
    test('Profile updates persist and are retrievable', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            initialNickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.record({
            nickname: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
            realName: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
            phone: fc.option(
              fc.integer({ min: 1000000, max: 999999999999999 }).map(n => '+' + n.toString())
            ),
            whatsapp: fc.option(
              fc.integer({ min: 1000000, max: 999999999999999 }).map(n => '+' + n.toString())
            ),
            wechatId: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
          }),
          async (userData, updateData) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.initialNickname,
            });

            // Filter out null values from updateData
            const filteredUpdateData = {};
            Object.keys(updateData).forEach(key => {
              if (updateData[key] !== null) {
                filteredUpdateData[key] = updateData[key];
              }
            });

            // Update profile
            const updatedUser = await mockUserService.updateUserProfile(user.id, filteredUpdateData);

            // Verify updated fields in response
            Object.keys(filteredUpdateData).forEach(key => {
              expect(updatedUser[key]).toBe(filteredUpdateData[key]);
            });

            // Retrieve profile to verify persistence
            const profile = await mockUserService.getUserProfile(user.id);

            // Verify all updated fields persisted
            Object.keys(filteredUpdateData).forEach(key => {
              expect(profile[key]).toBe(filteredUpdateData[key]);
            });
          }
        ),
        { numRuns: 100 }
      );
    });

    test('Multiple sequential updates maintain consistency', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.array(
            fc.record({
              nickname: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
              realName: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
            }),
            { minLength: 2, maxLength: 5 }
          ),
          async (userData, updateSequence) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
            });

            let expectedState = { nickname: userData.nickname };

            // Apply updates in sequence
            for (const update of updateSequence) {
              const filteredUpdate = {};
              Object.keys(update).forEach(key => {
                if (update[key] !== null) {
                  filteredUpdate[key] = update[key];
                  expectedState[key] = update[key];
                }
              });

              if (Object.keys(filteredUpdate).length > 0) {
                await mockUserService.updateUserProfile(user.id, filteredUpdate);
              }
            }

            // Verify final state matches expected
            const finalProfile = await mockUserService.getUserProfile(user.id);
            Object.keys(expectedState).forEach(key => {
              expect(finalProfile[key]).toBe(expectedState[key]);
            });
          }
        ),
        { numRuns: 50 }
      );
    });

    test('Updating non-existent user throws error', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.uuid(),
          fc.record({
            nickname: fc.string({ minLength: 1, maxLength: 100 }),
          }),
          async (nonExistentUserId, updateData) => {
            // Attempt to update non-existent user should throw
            await expect(
              mockUserService.updateUserProfile(nonExistentUserId, updateData)
            ).rejects.toThrow('User not found');
          }
        ),
        { numRuns: 50 }
      );
    });

    test('Invalid phone format is rejected', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.string({ minLength: 1, maxLength: 20 }).filter(
            s => !/^[+]?[\d\s-()]+$/.test(s) && s.length > 0
          ),
          async (userData, invalidPhone) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
            });

            // Attempt to update with invalid phone should throw
            await expect(
              mockUserService.updateUserProfile(user.id, { phone: invalidPhone })
            ).rejects.toThrow('Invalid phone number format');
          }
        ),
        { numRuns: 50 }
      );
    });

    test('Protected fields cannot be updated through profile update', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.record({
            wechatOpenId: fc.uuid(),
            nickname: fc.string({ minLength: 1, maxLength: 50 }),
          }),
          fc.record({
            balance: fc.double({ min: 0, max: 10000 }),
            status: fc.constantFrom('active', 'suspended'),
            wechatOpenId: fc.uuid(),
          }),
          async (userData, protectedFields) => {
            // Create a user
            const user = mockUserService.db.create({
              wechatOpenId: userData.wechatOpenId,
              nickname: userData.nickname,
              balance: 100.00,
              status: 'active',
            });

            const originalBalance = user.balance;
            const originalStatus = user.status;
            const originalWechatOpenId = user.wechatOpenId;

            // Attempt to update protected fields
            await mockUserService.updateUserProfile(user.id, protectedFields);

            // Verify protected fields remain unchanged
            const profile = await mockUserService.getUserProfile(user.id);
            expect(parseFloat(profile.balance)).toBe(parseFloat(originalBalance));
            expect(profile.status).toBe(originalStatus);
            expect(profile.wechatOpenId).toBe(originalWechatOpenId);
          }
        ),
        { numRuns: 50 }
      );
    });
  });
});
