using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Auth;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace HoneyBox.Tests.Services;

/// <summary>
/// AuthService手机号绑定属性测试
/// 测试手机号绑定相关的核心属性
/// </summary>
public class AuthServiceBindMobilePropertyTests
{
    private HoneyBoxDbContext CreateInMemoryDbContext()
    {
        var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
            .Options;

        return new HoneyBoxDbContext(options);
    }

    private (AuthService authService, Mock<IWechatService> wechatMock, Mock<IRedisService> redisMock) CreateAuthService(HoneyBoxDbContext dbContext)
    {
        var mockUserLogger = new Mock<ILogger<UserService>>();
        var userService = new UserService(dbContext, mockUserLogger.Object);

        var jwtSettings = new JwtSettings
        {
            Secret = "your-secret-key-must-be-at-least-32-characters-long-for-hs256",
            Issuer = "HoneyBox",
            Audience = "HoneyBoxUsers",
            ExpirationMinutes = 1440,
            RefreshTokenExpirationDays = 7
        };
        var mockJwtLogger = new Mock<ILogger<JwtService>>();
        var jwtService = new JwtService(jwtSettings, mockJwtLogger.Object);

        var mockWechatService = new Mock<IWechatService>();
        var mockIpLocationService = new Mock<IIpLocationService>();
        var mockRedisService = new Mock<IRedisService>();
        var mockAuthLogger = new Mock<ILogger<AuthService>>();

        mockIpLocationService.Setup(x => x.GetLocationAsync(It.IsAny<string>()))
            .ReturnsAsync(new IpLocationResult { Success = true, Province = "北京", City = "北京", Adcode = "110000" });

        var authService = new AuthService(
            dbContext,
            userService,
            jwtService,
            mockWechatService.Object,
            mockIpLocationService.Object,
            mockRedisService.Object,
            jwtSettings,
            mockAuthLogger.Object);

        return (authService, mockWechatService, mockRedisService);
    }

    /// <summary>
    /// Property 12: 手机号绑定验证码验证
    /// For any mobile binding request:
    /// - If verification code is correct, binding should succeed
    /// - If verification code is incorrect, binding should fail with "验证码错误"
    /// Validates: Requirements 5.1, 5.5
    /// Feature: user-auth-migration, Property 12: 手机号绑定验证码验证
    /// </summary>
    [Property(MaxTest = 100)]
    public async Task<bool> BindMobileVerificationCodeValidation()
    {
        var dbContext = CreateInMemoryDbContext();
        var (authService, _, redisMock) = CreateAuthService(dbContext);

        var mobile = "138" + Random.Shared.Next(10000000, 99999999).ToString();
        var correctCode = "123456";
        var wrongCode = "654321";

        // Create a user to bind mobile to
        var user = new User
        {
            OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
            Uid = "uid123",
            Nickname = "TestUser",
            HeadImg = "https://example.com/avatar.jpg",
            Status = 1,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };
        await dbContext.Users.AddAsync(user);
        await dbContext.SaveChangesAsync();

        // Test 1: Correct code should succeed
        redisMock.Setup(x => x.GetStringAsync($"sms:code:{mobile}"))
            .ReturnsAsync(correctCode);
        redisMock.Setup(x => x.DeleteAsync($"sms:code:{mobile}"))
            .Returns(Task.CompletedTask);

        var successResult = await authService.BindMobileAsync(user.Id, mobile, correctCode);
        var userAfterBind = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == user.Id);
        var bindSuccess = userAfterBind?.Mobile == mobile;

        // Test 2: Wrong code should fail
        var dbContext2 = CreateInMemoryDbContext();
        var (authService2, _, redisMock2) = CreateAuthService(dbContext2);

        var user2 = new User
        {
            OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
            Uid = "uid456",
            Nickname = "TestUser2",
            HeadImg = "https://example.com/avatar.jpg",
            Status = 1,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };
        await dbContext2.Users.AddAsync(user2);
        await dbContext2.SaveChangesAsync();

        redisMock2.Setup(x => x.GetStringAsync($"sms:code:{mobile}"))
            .ReturnsAsync(correctCode);

        bool wrongCodeFailed = false;
        try
        {
            await authService2.BindMobileAsync(user2.Id, mobile, wrongCode);
        }
        catch (InvalidOperationException ex) when (ex.Message == "验证码错误")
        {
            wrongCodeFailed = true;
        }

        return bindSuccess && wrongCodeFailed;
    }


    /// <summary>
    /// Property 13: 账户合并正确性
    /// When a mobile number is already bound to another user:
    /// - The current user's openid should be migrated to the mobile user
    /// - The current user record should be deleted
    /// - A new token should be returned for the mobile user
    /// Validates: Requirements 5.2, 5.3
    /// Feature: user-auth-migration, Property 13: 账户合并正确性
    /// </summary>
    [Property(MaxTest = 100)]
    public async Task<bool> AccountMergeCorrectness()
    {
        var dbContext = CreateInMemoryDbContext();
        var (authService, _, redisMock) = CreateAuthService(dbContext);

        var mobile = "138" + Random.Shared.Next(10000000, 99999999).ToString();
        var correctCode = "123456";
        var currentUserOpenId = "openid_current_" + Guid.NewGuid().ToString().Substring(0, 8);

        // Create current user (WeChat user without mobile)
        var currentUser = new User
        {
            OpenId = currentUserOpenId,
            Uid = "uid_current",
            Nickname = "CurrentUser",
            HeadImg = "https://example.com/avatar1.jpg",
            Status = 1,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };
        await dbContext.Users.AddAsync(currentUser);

        // Create mobile user (user with mobile already bound)
        var mobileUser = new User
        {
            OpenId = "openid_mobile",
            Mobile = mobile,
            Uid = "uid_mobile",
            Nickname = "MobileUser",
            HeadImg = "https://example.com/avatar2.jpg",
            Status = 1,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };
        await dbContext.Users.AddAsync(mobileUser);
        await dbContext.SaveChangesAsync();

        var currentUserId = currentUser.Id;
        var mobileUserId = mobileUser.Id;

        // Setup Redis mock
        redisMock.Setup(x => x.GetStringAsync($"sms:code:{mobile}"))
            .ReturnsAsync(correctCode);
        redisMock.Setup(x => x.DeleteAsync($"sms:code:{mobile}"))
            .Returns(Task.CompletedTask);

        // Act: Bind mobile (should trigger account merge)
        var result = await authService.BindMobileAsync(currentUserId, mobile, correctCode);

        // Assert
        // 1. Current user should be deleted
        var currentUserAfter = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == currentUserId);
        var currentUserDeleted = currentUserAfter == null;

        // 2. Mobile user should have the current user's openid
        var mobileUserAfter = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == mobileUserId);
        var openIdMigrated = mobileUserAfter?.OpenId == currentUserOpenId;

        // 3. New token should be returned
        var newTokenReturned = !string.IsNullOrWhiteSpace(result.Token);

        return currentUserDeleted && openIdMigrated && newTokenReturned;
    }

    /// <summary>
    /// Property 14: 直接绑定手机号
    /// When a mobile number is not bound to any other user:
    /// - The current user's mobile should be updated directly
    /// - No token should be returned (no account merge needed)
    /// Validates: Requirements 5.4
    /// Feature: user-auth-migration, Property 14: 直接绑定手机号
    /// </summary>
    [Property(MaxTest = 100)]
    public async Task<bool> DirectMobileBinding()
    {
        var dbContext = CreateInMemoryDbContext();
        var (authService, _, redisMock) = CreateAuthService(dbContext);

        var mobile = "138" + Random.Shared.Next(10000000, 99999999).ToString();
        var correctCode = "123456";

        // Create a user without mobile
        var user = new User
        {
            OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
            Uid = "uid123",
            Nickname = "TestUser",
            HeadImg = "https://example.com/avatar.jpg",
            Status = 1,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };
        await dbContext.Users.AddAsync(user);
        await dbContext.SaveChangesAsync();

        // Setup Redis mock
        redisMock.Setup(x => x.GetStringAsync($"sms:code:{mobile}"))
            .ReturnsAsync(correctCode);
        redisMock.Setup(x => x.DeleteAsync($"sms:code:{mobile}"))
            .Returns(Task.CompletedTask);

        // Act: Bind mobile (should be direct binding)
        var result = await authService.BindMobileAsync(user.Id, mobile, correctCode);

        // Assert
        // 1. User's mobile should be updated
        var userAfter = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == user.Id);
        var mobileUpdated = userAfter?.Mobile == mobile;

        // 2. No token should be returned (no merge)
        var noTokenReturned = string.IsNullOrWhiteSpace(result.Token);

        // 3. User count should remain the same
        var userCount = await dbContext.Users.CountAsync();
        var userCountCorrect = userCount == 1;

        return mobileUpdated && noTokenReturned && userCountCorrect;
    }
}
