import { type OAuthProvider } from '@prisma/client';
import { UserEntity } from '../../domain/entities/user.entity';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { Email } from '../../domain/value-objects/email.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
import { OAuthService, type OAuthUserProfile } from '../services/oauth.service';
import { type TokenPair } from '../services/token.service';

describe('OAuthService', () => {
  let service: OAuthService;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockTokenService: { generateTokenPair: ReturnType<typeof vi.fn> };
  let mockPrisma: {
    oAuthAccount: {
      findUnique: ReturnType<typeof vi.fn>;
      create: ReturnType<typeof vi.fn>;
      update: ReturnType<typeof vi.fn>;
    };
  };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  const mockTokenPair: TokenPair = {
    accessToken: 'test-jwt',
    refreshToken: 'family.token',
    expiresIn: 900,
  };

  const googleProfile: OAuthUserProfile = {
    provider: 'GOOGLE' as OAuthProvider,
    providerUserId: 'google-123',
    email: 'user@gmail.com',
    fullName: 'Test User',
    avatarUrl: 'https://photo.url',
    accessToken: 'google-access-token',
    refreshToken: 'google-refresh-token',
    rawProfile: { sub: 'google-123' },
  };

  const makeUserEntity = (id: string, phone: string, email?: string): UserEntity => {
    return new UserEntity(id, {
      email: email ? Email.create(email).unwrap() : null,
      phone: Phone.create(phone).unwrap(),
      passwordHash: null,
      fullName: 'Existing User',
      avatarUrl: null,
      role: 'BUYER',
      kycStatus: 'NONE',
      kycData: null,
      isActive: true,
    });
  };

  beforeEach(() => {
    mockUserRepo = {
      findById: vi.fn().mockResolvedValue(null),
      findByPhone: vi.fn().mockResolvedValue(null),
      findByEmail: vi.fn().mockResolvedValue(null),
      save: vi.fn().mockResolvedValue(undefined),
      update: vi.fn().mockResolvedValue(undefined),
    };

    mockTokenService = {
      generateTokenPair: vi.fn().mockResolvedValue(mockTokenPair),
    };

    mockPrisma = {
      oAuthAccount: {
        findUnique: vi.fn().mockResolvedValue(null),
        create: vi.fn().mockResolvedValue({}),
        update: vi.fn().mockResolvedValue({}),
      },
    };

    mockEventBus = { publish: vi.fn() };

    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };

    service = new OAuthService(
      mockUserRepo as any,
      mockTokenService as any,
      mockPrisma as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  describe('authenticateOAuth', () => {
    it('returns tokens for existing OAuth account', async () => {
      mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
        id: 'oauth-1',
        userId: 'user-1',
        provider: 'GOOGLE',
        accessToken: 'old-token',
        refreshToken: null,
        expiresAt: null,
        profile: null,
        user: { id: 'user-1', phone: '0912345678', role: 'BUYER', isActive: true },
      });

      const result = await service.authenticateOAuth(googleProfile);

      expect(result).toEqual(mockTokenPair);
      expect(mockPrisma.oAuthAccount.update).toHaveBeenCalled();
      expect(mockUserRepo.save).not.toHaveBeenCalled();
    });

    it('throws for deactivated user with existing OAuth account', async () => {
      mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
        id: 'oauth-1',
        userId: 'user-1',
        user: { id: 'user-1', phone: '0912345678', role: 'BUYER', isActive: false },
      });

      await expect(service.authenticateOAuth(googleProfile)).rejects.toThrow(
        'Tài khoản đã bị vô hiệu hóa',
      );
    });

    it('links OAuth account to existing user by email', async () => {
      const existingUser = makeUserEntity('user-2', '+84912345678', 'user@gmail.com');
      mockUserRepo.findByEmail.mockResolvedValue(existingUser);

      const result = await service.authenticateOAuth(googleProfile);

      expect(result).toEqual(mockTokenPair);
      expect(mockPrisma.oAuthAccount.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            userId: 'user-2',
            provider: 'GOOGLE',
            providerUserId: 'google-123',
          }),
        }),
      );
      expect(mockUserRepo.save).not.toHaveBeenCalled();
    });

    it('links OAuth account to existing user by phone', async () => {
      const profileWithPhone: OAuthUserProfile = {
        ...googleProfile,
        email: undefined,
        phone: '+84912345678',
      };
      const existingUser = makeUserEntity('user-3', '+84912345678');
      mockUserRepo.findByPhone.mockResolvedValue(existingUser);

      const result = await service.authenticateOAuth(profileWithPhone);

      expect(result).toEqual(mockTokenPair);
      expect(mockPrisma.oAuthAccount.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ userId: 'user-3' }),
        }),
      );
    });

    it('creates new user when no existing account matches', async () => {
      const result = await service.authenticateOAuth(googleProfile);

      expect(result).toEqual(mockTokenPair);
      expect(mockUserRepo.save).toHaveBeenCalled();
      expect(mockPrisma.oAuthAccount.create).toHaveBeenCalled();
      expect(mockEventBus.publish).toHaveBeenCalled();
    });

    it('creates new user without password hash for OAuth accounts', async () => {
      await service.authenticateOAuth(googleProfile);

      const savedUser = mockUserRepo.save.mock.calls[0][0] as UserEntity;
      expect(savedUser.passwordHash).toBeNull();
      expect(savedUser.fullName).toBe('Test User');
    });

    it('throws for deactivated user found by email link', async () => {
      const deactivatedUser = makeUserEntity('user-4', '+84912345678', 'user@gmail.com');
      deactivatedUser.deactivate();
      mockUserRepo.findByEmail.mockResolvedValue(deactivatedUser);

      await expect(service.authenticateOAuth(googleProfile)).rejects.toThrow(
        'Tài khoản đã bị vô hiệu hóa',
      );
    });
  });
});
