import { Test, TestingModule } from '@nestjs/testing';
import { UserApplicationService } from './user-application.service';
import {
  USER_ACCOUNT_REPOSITORY,
  UserAccountRepository,
  ReferralLinkData,
  CreateReferralLinkParams,
} from '@/domain/repositories/user-account.repository.interface';
import {
  MPC_KEY_SHARE_REPOSITORY,
  MpcKeyShareRepository,
} from '@/domain/repositories/mpc-key-share.repository.interface';
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
import {
  AccountSequence,
  ReferralCode,
  UserId,
  AccountStatus,
  KYCStatus,
  DeviceInfo,
} from '@/domain/value-objects';
import { ConfigService } from '@nestjs/config';
import {
  ValidateReferralCodeQuery,
  GetReferralStatsQuery,
  GenerateReferralLinkCommand,
} from '@/application/commands';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
import {
  AccountSequenceGeneratorService,
  UserValidatorService,
} from '@/domain/services';
import { TokenService } from './token.service';
import { RedisService } from '@/infrastructure/redis/redis.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { MpcWalletService } from '@/infrastructure/external/mpc';
import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service';
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
import { BlockchainWalletHandler } from '@/application/event-handlers/blockchain-wallet.handler';
import { MpcKeygenCompletedHandler } from '@/application/event-handlers/mpc-keygen-completed.handler';

describe('UserApplicationService - Referral APIs', () => {
  let service: UserApplicationService;
  let mockUserRepository: jest.Mocked<UserAccountRepository>;

  // Helper function to create a test account using UserAccount.reconstruct
  const createMockAccount = (
    params: {
      userId?: string;
      accountSequence?: string;
      referralCode?: string;
      nickname?: string;
      avatarUrl?: string | null;
      isActive?: boolean;
      inviterSequence?: string | null;
      registeredAt?: Date;
    } = {},
  ): UserAccount => {
    const devices = [
      new DeviceInfo('device-001', 'Test Device', new Date(), new Date()),
    ];

    return UserAccount.reconstruct({
      userId: params.userId || '123456789',
      accountSequence: params.accountSequence || 'D2412190001',
      devices,
      phoneNumber: '13800138000',
      nickname: params.nickname || '用户1',
      avatarUrl: params.avatarUrl ?? null,
      inviterSequence: params.inviterSequence ?? null,
      referralCode: params.referralCode || 'ABC123',
      walletAddresses: [],
      kycInfo: null,
      kycStatus: KYCStatus.NOT_VERIFIED,
      status:
        params.isActive !== false ? AccountStatus.ACTIVE : AccountStatus.FROZEN,
      registeredAt: params.registeredAt || new Date(),
      lastLoginAt: null,
      updatedAt: new Date(),
    });
  };

  beforeEach(async () => {
    mockUserRepository = {
      save: jest.fn(),
      saveWallets: jest.fn(),
      findById: jest.fn(),
      findByAccountSequence: jest.fn(),
      findByDeviceId: jest.fn(),
      findByPhoneNumber: jest.fn(),
      findByReferralCode: jest.fn(),
      findByWalletAddress: jest.fn(),
      getMaxAccountSequence: jest.fn(),
      getNextAccountSequence: jest.fn(),
      findUsers: jest.fn(),
      countUsers: jest.fn(),
      findByUserIds: jest.fn(),
      findByInviterSequence: jest.fn(),
      createReferralLink: jest.fn(),
      findReferralLinksByUserId: jest.fn(),
      findUsersWithIncompleteWallets: jest.fn(),
    };

    const mockMpcKeyShareRepository: jest.Mocked<MpcKeyShareRepository> = {
      saveServerShare: jest.fn(),
      findByUserId: jest.fn(),
      findByPublicKey: jest.fn(),
      updateStatus: jest.fn(),
      rotateShare: jest.fn(),
    };

    const mockConfigService = {
      get: jest.fn((key: string) => {
        const config: Record<string, any> = {
          APP_BASE_URL: 'https://app.rwadurian.com',
          MPC_MODE: 'local',
        };
        return config[key];
      }),
    };

    const mockAccountSequenceGeneratorService = {
      getNext: jest
        .fn()
        .mockResolvedValue(AccountSequence.create('D2412190001')),
    };

    const mockUserValidatorService = {
      validateUniquePhone: jest.fn(),
    };

    const mockBlockchainClientService = {
      getBalance: jest.fn(),
    };

    const mockTokenService = {
      generateAccessToken: jest.fn().mockReturnValue('mock-access-token'),
      generateRefreshToken: jest.fn().mockReturnValue('mock-refresh-token'),
      generateDeviceRefreshToken: jest
        .fn()
        .mockReturnValue('mock-device-refresh-token'),
      verifyRefreshToken: jest.fn(),
    };

    const mockRedisService = {
      get: jest.fn(),
      set: jest.fn(),
      del: jest.fn(),
      setWithExpiry: jest.fn(),
    };

    const mockSmsService = {
      sendSmsCode: jest.fn(),
    };

    const mockEventPublisherService = {
      publish: jest.fn(),
    };

    const mockMpcWalletService = {
      generateMpcWallet: jest.fn(),
    };

    const mockPrismaService = {
      userAccount: { findUnique: jest.fn() },
    };

    const mockBlockchainWalletHandler = {
      handle: jest.fn(),
    };

    const mockMpcKeygenCompletedHandler = {
      handle: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserApplicationService,
        {
          provide: USER_ACCOUNT_REPOSITORY,
          useValue: mockUserRepository,
        },
        {
          provide: MPC_KEY_SHARE_REPOSITORY,
          useValue: mockMpcKeyShareRepository,
        },
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
        {
          provide: AccountSequenceGeneratorService,
          useValue: mockAccountSequenceGeneratorService,
        },
        {
          provide: UserValidatorService,
          useValue: mockUserValidatorService,
        },
        {
          provide: BlockchainClientService,
          useValue: mockBlockchainClientService,
        },
        {
          provide: TokenService,
          useValue: mockTokenService,
        },
        {
          provide: RedisService,
          useValue: mockRedisService,
        },
        {
          provide: SmsService,
          useValue: mockSmsService,
        },
        {
          provide: EventPublisherService,
          useValue: mockEventPublisherService,
        },
        {
          provide: MpcWalletService,
          useValue: mockMpcWalletService,
        },
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
        {
          provide: BlockchainWalletHandler,
          useValue: mockBlockchainWalletHandler,
        },
        {
          provide: MpcKeygenCompletedHandler,
          useValue: mockMpcKeygenCompletedHandler,
        },
      ],
    }).compile();

    service = module.get<UserApplicationService>(UserApplicationService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  // ============ GET /api/me Tests ============
  describe('getMe', () => {
    it('should return current user info with referral code and link', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        accountSequence: 'D2412190001',
        nickname: '测试用户',
        referralCode: 'ABC123',
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);

      const result = await service.getMe('123456789');

      expect(result).toEqual({
        userId: '123456789',
        accountSequence: 'D2412190001',
        phoneNumber: '138****8000', // masked
        nickname: '测试用户',
        avatarUrl: null,
        referralCode: 'ABC123',
        referralLink: 'https://app.rwadurian.com/invite/ABC123',
        walletAddresses: [],
        kycStatus: KYCStatus.NOT_VERIFIED,
        status: AccountStatus.ACTIVE,
        registeredAt: expect.any(Date),
      });

      expect(mockUserRepository.findById).toHaveBeenCalledWith(
        expect.any(UserId),
      );
    });

    it('should throw error when user not found', async () => {
      mockUserRepository.findById.mockResolvedValue(null);

      // Use valid numeric string for userId
      await expect(service.getMe('999999999')).rejects.toThrow(
        ApplicationError,
      );
      await expect(service.getMe('999999999')).rejects.toThrow('用户不存在');
    });
  });

  // ============ GET /api/referrals/validate Tests ============
  describe('validateReferralCode', () => {
    it('should return valid=true for existing active referral code', async () => {
      const mockInviter = createMockAccount({
        accountSequence: 'D2412190100',
        nickname: '邀请人',
        avatarUrl: 'https://example.com/avatar.jpg',
        referralCode: 'INVTE1',
        isActive: true,
      });

      mockUserRepository.findByReferralCode.mockResolvedValue(mockInviter);

      const result = await service.validateReferralCode(
        new ValidateReferralCodeQuery('INVTE1'),
      );

      expect(result).toEqual({
        valid: true,
        referralCode: 'INVTE1',
        inviterInfo: {
          accountSequence: 'D2412190100',
          nickname: '邀请人',
          avatarUrl: 'https://example.com/avatar.jpg',
        },
      });
    });

    it('should return valid=false for non-existent referral code', async () => {
      mockUserRepository.findByReferralCode.mockResolvedValue(null);

      const result = await service.validateReferralCode(
        new ValidateReferralCodeQuery('INVLD1'),
      );

      expect(result).toEqual({
        valid: false,
        message: '推荐码不存在',
      });
    });

    it('should return valid=false for frozen inviter account', async () => {
      const frozenInviter = createMockAccount({
        referralCode: 'FROZN1',
        isActive: false,
      });

      mockUserRepository.findByReferralCode.mockResolvedValue(frozenInviter);

      const result = await service.validateReferralCode(
        new ValidateReferralCodeQuery('FROZN1'),
      );

      expect(result).toEqual({
        valid: false,
        message: '推荐人账户已冻结',
      });
    });

    it('should return valid=false for invalid referral code format', async () => {
      const result = await service.validateReferralCode(
        new ValidateReferralCodeQuery('invalid-format-too-long'),
      );

      expect(result.valid).toBe(false);
      expect(result.message).toBe('推荐码格式无效');
    });
  });

  // ============ POST /api/referrals/links Tests ============
  describe('generateReferralLink', () => {
    it('should generate a new referral link with channel', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        referralCode: 'ABC123',
      });

      const mockLinkData: ReferralLinkData = {
        linkId: BigInt(1),
        userId: BigInt(123456789),
        referralCode: 'ABC123',
        shortCode: 'XyZ789',
        channel: 'wechat',
        campaignId: null,
        createdAt: new Date('2024-01-15T10:00:00Z'),
      };

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.createReferralLink.mockResolvedValue(mockLinkData);

      const result = await service.generateReferralLink(
        new GenerateReferralLinkCommand('123456789', 'wechat'),
      );

      expect(result).toEqual({
        linkId: '1',
        referralCode: 'ABC123',
        shortUrl: expect.stringMatching(
          /^https:\/\/app\.rwadurian\.com\/r\/[A-Za-z0-9]{6}$/,
        ),
        fullUrl: 'https://app.rwadurian.com/invite/ABC123?ch=wechat',
        channel: 'wechat',
        campaignId: null,
        createdAt: expect.any(Date),
      });

      expect(mockUserRepository.createReferralLink).toHaveBeenCalledWith({
        userId: expect.any(BigInt),
        referralCode: 'ABC123',
        shortCode: expect.any(String),
        channel: 'wechat',
        campaignId: null,
      });
    });

    it('should generate a referral link with campaign ID', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        referralCode: 'ABC123',
      });

      const mockLinkData: ReferralLinkData = {
        linkId: BigInt(2),
        userId: BigInt(123456789),
        referralCode: 'ABC123',
        shortCode: 'AbC456',
        channel: 'telegram',
        campaignId: 'spring2024',
        createdAt: new Date('2024-01-15T10:00:00Z'),
      };

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.createReferralLink.mockResolvedValue(mockLinkData);

      const result = await service.generateReferralLink(
        new GenerateReferralLinkCommand('123456789', 'telegram', 'spring2024'),
      );

      expect(result.channel).toBe('telegram');
      expect(result.campaignId).toBe('spring2024');
    });

    it('should generate link with default channel when not specified', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        referralCode: 'ABC123',
      });

      const mockLinkData: ReferralLinkData = {
        linkId: BigInt(3),
        userId: BigInt(123456789),
        referralCode: 'ABC123',
        shortCode: 'DeF789',
        channel: null,
        campaignId: null,
        createdAt: new Date(),
      };

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.createReferralLink.mockResolvedValue(mockLinkData);

      const result = await service.generateReferralLink(
        new GenerateReferralLinkCommand('123456789'),
      );

      expect(result.fullUrl).toContain('ch=default');
      expect(result.channel).toBeNull();
    });

    it('should throw error when user not found', async () => {
      mockUserRepository.findById.mockResolvedValue(null);

      // Use valid numeric string for userId
      await expect(
        service.generateReferralLink(
          new GenerateReferralLinkCommand('999999999'),
        ),
      ).rejects.toThrow(ApplicationError);
    });
  });

  // ============ GET /api/referrals/stats Tests ============
  describe('getReferralStats', () => {
    it('should return referral stats with direct and indirect invites', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        accountSequence: 'D2412190001',
        referralCode: 'ABC123',
      });

      // Direct invites (invited by user 1)
      const directInvite1 = createMockAccount({
        userId: '200000001',
        accountSequence: 'D2412190002',
        nickname: '直接邀请1',
        inviterSequence: 'D2412190001',
        registeredAt: new Date(),
      });

      const directInvite2 = createMockAccount({
        userId: '200000002',
        accountSequence: 'D2412190003',
        nickname: '直接邀请2',
        inviterSequence: 'D2412190001',
        registeredAt: new Date(),
      });

      // Indirect invite (invited by user 2, who was invited by user 1)
      const indirectInvite1 = createMockAccount({
        userId: '300000001',
        accountSequence: 'D2412190004',
        nickname: '间接邀请1',
        inviterSequence: 'D2412190002',
        registeredAt: new Date(),
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.findByInviterSequence
        .mockResolvedValueOnce([directInvite1, directInvite2]) // Direct invites of user 1
        .mockResolvedValueOnce([indirectInvite1]) // Indirect invites via user 2
        .mockResolvedValueOnce([]); // Indirect invites via user 3 (none)

      const result = await service.getReferralStats(
        new GetReferralStatsQuery('123456789'),
      );

      expect(result).toEqual({
        referralCode: 'ABC123',
        totalInvites: 3, // 2 direct + 1 indirect
        directInvites: 2,
        indirectInvites: 1,
        todayInvites: expect.any(Number),
        thisWeekInvites: expect.any(Number),
        thisMonthInvites: expect.any(Number),
        recentInvites: expect.arrayContaining([
          expect.objectContaining({
            accountSequence: expect.any(String),
            nickname: expect.any(String),
            level: expect.any(Number), // 1 for direct, 2 for indirect
          }),
        ]),
      });

      expect(result.recentInvites.length).toBeLessThanOrEqual(20);
    });

    it('should return empty stats when no invites', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        accountSequence: 'D2412190001',
        referralCode: 'ABC123',
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.findByInviterSequence.mockResolvedValue([]);

      const result = await service.getReferralStats(
        new GetReferralStatsQuery('123456789'),
      );

      expect(result).toEqual({
        referralCode: 'ABC123',
        totalInvites: 0,
        directInvites: 0,
        indirectInvites: 0,
        todayInvites: 0,
        thisWeekInvites: 0,
        thisMonthInvites: 0,
        recentInvites: [],
      });
    });

    it('should correctly calculate time-based stats', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        accountSequence: 'D2412190001',
        referralCode: 'ABC123',
      });

      const now = new Date();
      const todayInvite = createMockAccount({
        accountSequence: 'D2412190002',
        nickname: '今日邀请',
        inviterSequence: 'D2412190001',
        registeredAt: now,
      });

      const yesterdayInvite = createMockAccount({
        accountSequence: 'D2412190003',
        nickname: '昨日邀请',
        inviterSequence: 'D2412190001',
        registeredAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), // yesterday
      });

      const lastMonthInvite = createMockAccount({
        accountSequence: 'D2412190004',
        nickname: '上月邀请',
        inviterSequence: 'D2412190001',
        registeredAt: new Date(now.getFullYear(), now.getMonth() - 1, 15),
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.findByInviterSequence
        .mockResolvedValueOnce([todayInvite, yesterdayInvite, lastMonthInvite])
        .mockResolvedValue([]); // No second-level invites

      const result = await service.getReferralStats(
        new GetReferralStatsQuery('123456789'),
      );

      expect(result.directInvites).toBe(3);
      expect(result.todayInvites).toBe(1); // Only today's invite
    });

    it('should throw error when user not found', async () => {
      mockUserRepository.findById.mockResolvedValue(null);

      // Use valid numeric string for userId
      await expect(
        service.getReferralStats(new GetReferralStatsQuery('999999999')),
      ).rejects.toThrow(ApplicationError);
    });

    it('should sort recent invites by registration date (newest first)', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        accountSequence: 'D2412190001',
        referralCode: 'ABC123',
      });

      const now = new Date();
      const oldInvite = createMockAccount({
        accountSequence: 'D2412190002',
        nickname: '旧邀请',
        inviterSequence: 'D2412190001',
        registeredAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
      });

      const newInvite = createMockAccount({
        accountSequence: 'D2412190003',
        nickname: '新邀请',
        inviterSequence: 'D2412190001',
        registeredAt: now,
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);
      mockUserRepository.findByInviterSequence
        .mockResolvedValueOnce([oldInvite, newInvite])
        .mockResolvedValue([]);

      const result = await service.getReferralStats(
        new GetReferralStatsQuery('123456789'),
      );

      // Newest should be first
      expect(result.recentInvites[0].nickname).toBe('新邀请');
      expect(result.recentInvites[1].nickname).toBe('旧邀请');
    });
  });

  // ============ Short Code Generation Tests ============
  describe('generateShortCode (private method behavior)', () => {
    it('should generate short codes with 6 characters', async () => {
      const mockAccount = createMockAccount({
        userId: '123456789',
        referralCode: 'ABC123',
      });

      mockUserRepository.findById.mockResolvedValue(mockAccount);

      // Generate multiple links and verify short codes have correct length
      for (let i = 0; i < 5; i++) {
        const mockLinkData: ReferralLinkData = {
          linkId: BigInt(i + 1),
          userId: BigInt(123456789),
          referralCode: 'ABC123',
          shortCode: `code${i}`,
          channel: null,
          campaignId: null,
          createdAt: new Date(),
        };

        mockUserRepository.createReferralLink.mockResolvedValueOnce(
          mockLinkData,
        );

        await service.generateReferralLink(
          new GenerateReferralLinkCommand('123456789', `channel${i}`),
        );
      }

      // All generated short codes should have 6 characters
      const createReferralLinkCalls =
        mockUserRepository.createReferralLink.mock.calls;
      createReferralLinkCalls.forEach((call) => {
        const params = call[0] as CreateReferralLinkParams;
        expect(params.shortCode).toHaveLength(6);
        // Should not contain confusing characters (I, l, O, 0, 1)
        expect(params.shortCode).not.toMatch(/[IlO01]/);
      });
    });
  });
});
