/**
 * AuthService Unit Tests (v2.1.1)
 * 
 * 测试认证服务
 * 
 * 测试覆盖:
 * - 登录（本地、LDAP、Entra ID）
 * - 登录失败锁定机制（v2.1.1）
 * - 登出
 * - Token刷新
 * - 密码修改（身份源限制，v2.1.1）
 * - 权限验证
 * - 用户状态检查
 * 
 * 基于文档: docs/modules/organization/09-test-scenarios.md
 * 版本: v2.1.1
 */

import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException, ForbiddenException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '@/modules/organization/auth/auth.service';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { LdapService } from '@/modules/organization/ldap/ldap.service';

// Mock bcrypt module
jest.mock('bcrypt', () => ({
  hash: jest.fn(),
  compare: jest.fn(),
}));

import * as bcrypt from 'bcrypt';

describe('AuthService', () => {
  let service: AuthService;
  let jwtService: JwtService;
  let prisma: PrismaService;
  let ldapService: LdapService;
  let configService: ConfigService;

  const mockUser = {
    id: 'user-test-001',
    username: 'testuser',
    email: 'test@example.com',
    displayName: 'Test User',
    passwordHash: '$2b$10$hashedpassword',
    status: 'ACTIVE',
    source: 'LOCAL',
    isActive: true,
    deletedAt: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    roles: [{
      role: {
        id: 'role-001',
        code: 'USER',
        name: 'User',
        permissions: [
          {
            permission: {
              resource: 'user',
              action: 'read',
            },
          },
        ],
      },
    }],
    departmentMemberships: [{
      isPrimary: true,
      department: { name: 'Engineering' },
      position: { name: 'Developer' },
    }],
  };

  const mockToken = {
    accessToken: 'mock-access-token',
    refreshToken: 'mock-refresh-token',
    tokenType: 'Bearer',
    expiresIn: 3600,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: JwtService,
          useValue: {
            sign: jest.fn(),
            verify: jest.fn(),
            decode: jest.fn(),
          },
        },
        {
          provide: LdapService,
          useValue: {
            authenticate: jest.fn(),
            searchUser: jest.fn(),
          },
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string) => {
              const config: Record<string, string> = {
                'jwt.secret': 'test-secret',
                'jwt.accessExpiresIn': '1h',
                'jwt.refreshExpiresIn': '7d',
              };
              return config[key];
            }),
          },
        },
        {
          provide: PrismaService,
          useValue: {
            user: {
              findFirst: jest.fn(),
              findUnique: jest.fn(),
              findMany: jest.fn(),
              create: jest.fn(),
              update: jest.fn(),
            },
            refreshToken: {
              create: jest.fn(),
              findUnique: jest.fn(),
              delete: jest.fn(),
              deleteMany: jest.fn(),
            },
          },
        },
      ],
    }).compile();

    service = module.get<AuthService>(AuthService);
    jwtService = module.get<JwtService>(JwtService);
    prisma = module.get<PrismaService>(PrismaService);
    ldapService = module.get<LdapService>(LdapService);
    configService = module.get<ConfigService>(ConfigService);

    // Mock private methods for all tests
    jest.spyOn(service as any, 'generateToken').mockResolvedValue({
      accessToken: 'mock-access-token',
      refreshToken: 'mock-refresh-token',
      tokenType: 'Bearer',
      expiresIn: 3600,
    });
    jest.spyOn(service as any, 'getAccessibleRegions').mockResolvedValue(['CN']);
    jest.spyOn(service as any, 'getRegionPermissions').mockResolvedValue({
      CN: ['user:read'],
    });
  });

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

  describe('login', () => {
    it('[测试场景 5.1.1] 应该成功登录本地用户', async () => {
      const password = 'test123';
      const hashedPassword = '$2b$10$hashedpassword';

      const userWithHashedPwd = {
        ...mockUser,
        passwordHash: hashedPassword,
        source: 'LOCAL',
      };

      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(userWithHashedPwd as any);
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);

      const result = await service.login({
        username: 'testuser',
        password,
      });

      expect(result.token).toBeDefined();
      expect(result.token.accessToken).toBe('mock-access-token');
      expect(result.user.id).toBe(mockUser.id);
      expect(result.user.username).toBe(mockUser.username);

      expect(prisma.user.findFirst).toHaveBeenCalled();
      expect(bcrypt.compare).toHaveBeenCalledWith(password, hashedPassword);
    });

    it('[测试场景 5.1.2] LDAP 用户成功登录（v2.1.1）', async () => {
      const ldapUser = {
        ...mockUser,
        username: 'ldapuser',
        source: 'LDAP',
        ldapDn: 'CN=LDAP User,OU=Users,DC=company,DC=com',
        passwordHash: null,
      };

      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(ldapUser as any);
      jest.spyOn(ldapService, 'authenticate').mockResolvedValue({
        success: true,
        user: ldapUser,
      } as any);

      const result = await service.login({
        username: 'ldapuser',
        password: 'ldappass',
      });

      expect(result.token).toBeDefined();
      expect(result.user.source).toBe('LDAP');
      expect(ldapService.authenticate).toHaveBeenCalledWith('ldapuser', 'ldappass');
    });

    it('[测试场景 5.1.3] Entra ID 用户不能登录（v2.1.1）', async () => {
      const entraUser = {
        ...mockUser,
        username: 'entrauser',
        source: 'ENTRA',
        externalId: 'entra-id-12345',
        externalSource: 'ENTRA_ID',
        passwordHash: null,
      };

      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(entraUser as any);

      await expect(
        service.login({
          username: 'entrauser',
          password: 'anypassword',
        }),
      ).rejects.toThrow(UnauthorizedException);
      // 应该包含错误码 ORG-AUTH-1008
    });

    it('[测试场景 5.1.4] 密码错误应抛出异常', async () => {
      const password = 'wrong-password';
      const correctHash = '$2b$10$correcthash';

      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue({
        ...mockUser,
        passwordHash: correctHash,
        source: 'LOCAL',
      } as any);
      (bcrypt.compare as jest.Mock).mockResolvedValue(false);

      await expect(
        service.login({
          username: 'testuser',
          password,
        }),
      ).rejects.toThrow(UnauthorizedException);

      expect(bcrypt.compare).toHaveBeenCalledWith(password, correctHash);
    });

    it('用户不存在应抛出异常', async () => {
      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(null);

      await expect(
        service.login({
          username: 'nonexistent',
          password: 'test123',
        }),
      ).rejects.toThrow(UnauthorizedException);
    });

    it('[测试场景 5.1.5] 停用用户无法登录', async () => {
      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(null); // Query filters out INACTIVE users

      await expect(
        service.login({
          username: 'inactiveuser',
          password: 'test123',
        }),
      ).rejects.toThrow(UnauthorizedException); // Not found, treated as invalid credentials
    });

    it('终止用户无法登录', async () => {
      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(null); // Query filters out TERMINATED users

      await expect(
        service.login({
          username: 'terminated',
          password: 'test123',
        }),
      ).rejects.toThrow(UnauthorizedException);
    });

    describe('[测试场景 5.1.6] 登录失败锁定机制（v2.1.1）', () => {
      beforeEach(() => {
        // Mock Redis methods for lockout testing
        (service as any).redis = {
          get: jest.fn(),
          set: jest.fn(),
          del: jest.fn(),
          incr: jest.fn(),
          expire: jest.fn(),
        };
      });

      it('普通用户5次失败后应锁定账号', async () => {
        const regularUser = {
          ...mockUser,
          username: 'regularuser',
          source: 'LOCAL',
          roles: [], // 普通用户，无管理员角色
        };

        jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(regularUser as any);
        (bcrypt.compare as jest.Mock).mockResolvedValue(false);

        // Mock Redis 返回失败次数
        const mockRedis = (service as any).redis;
        mockRedis.get.mockResolvedValue('5'); // 已失败5次
        mockRedis.incr.mockResolvedValue(6);

        await expect(
          service.login({
            username: 'regularuser',
            password: 'wrong',
          }),
        ).rejects.toThrow(UnauthorizedException);
        // 应该包含错误码 ORG-AUTH-1007 和锁定信息
      });

      it('管理员3次失败后应锁定账号', async () => {
        const adminUser = {
          ...mockUser,
          username: 'admin',
          source: 'LOCAL',
          roles: [{
            role: {
              code: 'ADMIN',
              name: 'Administrator',
            },
          }],
        };

        jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(adminUser as any);
        (bcrypt.compare as jest.Mock).mockResolvedValue(false);

        // Mock Redis 返回失败次数
        const mockRedis = (service as any).redis;
        mockRedis.get.mockResolvedValue('3'); // 已失败3次
        mockRedis.incr.mockResolvedValue(4);

        await expect(
          service.login({
            username: 'admin',
            password: 'wrong',
          }),
        ).rejects.toThrow(UnauthorizedException);
        // 应该包含错误码 ORG-AUTH-1007
      });

      it('应该显示剩余尝试次数', async () => {
        const user = {
          ...mockUser,
          source: 'LOCAL',
          roles: [],
        };

        jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(user as any);
        (bcrypt.compare as jest.Mock).mockResolvedValue(false);

        // Mock Redis 返回失败次数
        const mockRedis = (service as any).redis;
        mockRedis.get.mockResolvedValue('3'); // 已失败3次
        mockRedis.incr.mockResolvedValue(4);

        try {
          await service.login({
            username: 'testuser',
            password: 'wrong',
          });
        } catch (error: any) {
          expect(error).toBeInstanceOf(UnauthorizedException);
          // 应该在错误详情中包含剩余尝试次数 (5 - 4 = 1)
          expect(error.message).toBeDefined();
        }
      });

      it('锁定期过后应该可以重新登录', async () => {
        const user = {
          ...mockUser,
          source: 'LOCAL',
          passwordHash: '$2b$10$hashedpassword',
        };

        jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(user as any);
        (bcrypt.compare as jest.Mock).mockResolvedValue(true);

        // Mock Redis 显示锁定已过期（返回 null）
        const mockRedis = (service as any).redis;
        mockRedis.get.mockResolvedValue(null);

        const result = await service.login({
          username: 'testuser',
          password: 'correct',
        });

        expect(result.token).toBeDefined();
      });
    });
  });

  describe('refresh', () => {
    it('应该成功刷新Token', async () => {
      const oldAccessToken = 'old-access-token';
      const userId = mockUser.id;

      jest.spyOn(jwtService, 'verify').mockReturnValue({
        sub: userId,
        username: mockUser.username,
        iat: Date.now(),
      });
      jest.spyOn(jwtService, 'decode').mockReturnValue({
        sub: userId,
        username: mockUser.username,
      });
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser as any);
      
      // Override the beforeEach mock
      (service as any).generateToken = jest.fn().mockResolvedValue({
        accessToken: 'new-access-token',
        refreshToken: 'new-refresh-token',
        tokenType: 'Bearer',
        expiresIn: 3600,
      });

      const result = await service.refresh(oldAccessToken);

      expect(result.accessToken).toBe('new-access-token');
    });

    it('无效的access token应抛出异常', async () => {
      jest.spyOn(jwtService, 'verify').mockImplementation(() => {
        throw new Error('Invalid token');
      });

      await expect(service.refresh('invalid-token')).rejects.toThrow(UnauthorizedException);
    });

    it('用户不存在应抛出异常', async () => {
      jest.spyOn(jwtService, 'verify').mockReturnValue({
        sub: 'non-existent-user',
        username: 'test',
      });
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      await expect(service.refresh('valid-token')).rejects.toThrow(UnauthorizedException);
    });
  });

  describe('logout', () => {
    it('应该成功登出', async () => {
      const accessToken = 'test-access-token';
      
      jest.spyOn(jwtService, 'verify').mockReturnValue({
        sub: 'user-001',
        username: 'test',
      });

      const result = await service.logout(accessToken);

      expect(result.message).toBe('Logged out successfully');
    });
  });

  describe('validateUser', () => {
    it('应该验证有效的用户', async () => {
      const mockUserWithRoles = {
        ...mockUser,
        roles: [
          {
            organizationId: null,  // 全局角色
            region: null,  // 全局角色（向后兼容）
            role: {
              code: 'ADMIN',
              permissions: [
                {
                  permission: {
                    resource: 'user',
                    action: 'read',
                  },
                },
              ],
            },
          },
        ],
      };
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUserWithRoles as any);

      const result = await service.validateUser(mockUser.id);

      if (result) {
        expect(result).toBeDefined();
        expect(result.userId).toBe(mockUser.id);
        expect(result.roles).toContain('ADMIN');
      }
    });

    it('应该拒绝停用的用户', async () => {
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      const result = await service.validateUser(mockUser.id);

      expect(result).toBeNull();
    });

    it('应该拒绝不存在的用户', async () => {
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      const result = await service.validateUser('non-existent');
      expect(result).toBeNull();
    });
  });

  describe('changePassword', () => {
    it('[测试场景 5.2.1] LOCAL 用户成功修改密码', async () => {
      const userId = mockUser.id;
      const oldPassword = 'OldPassword123';
      const newPassword = 'NewPassword456';
      const oldHash = '$2b$10$oldhash';
      const newHash = '$2b$10$newhash';

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({
        ...mockUser,
        source: 'LOCAL',
        passwordHash: oldHash,
      } as any);
      // bcrypt.compare 第一次调用：验证旧密码正确
      // 第二次调用：验证新密码与旧密码不同
      (bcrypt.compare as jest.Mock)
        .mockResolvedValueOnce(true)   // 旧密码验证通过
        .mockResolvedValueOnce(false); // 新密码与旧密码不同
      (bcrypt.hash as jest.Mock).mockResolvedValue(newHash);
      jest.spyOn(prisma.user, 'update').mockResolvedValue(mockUser as any);

      await service.changePassword(userId, {
        oldPassword,
        newPassword,
      });

      expect(prisma.user.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: userId },
          data: expect.objectContaining({ passwordHash: newHash }),
        })
      );
      expect(bcrypt.hash).toHaveBeenCalledWith(newPassword, 10);
    });

    it('[测试场景 5.2.2] LDAP 用户不能修改密码（v2.1.1）', async () => {
      const ldapUser = {
        ...mockUser,
        source: 'LDAP',
        ldapDn: 'CN=LDAP User,OU=Users,DC=company,DC=com',
        passwordHash: null,
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(ldapUser as any);

      await expect(
        service.changePassword(mockUser.id, {
          oldPassword: 'old',
          newPassword: 'new',
        }),
      ).rejects.toThrow(BadRequestException);
      // 应该包含错误码 ORG-AUTH-1009
      
      expect(prisma.user.update).not.toHaveBeenCalled();
    });

    it('[测试场景 5.2.3] Entra ID 用户不能修改密码（v2.1.1）', async () => {
      const entraUser = {
        ...mockUser,
        source: 'ENTRA',
        externalId: 'entra-id-12345',
        passwordHash: null,
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(entraUser as any);

      await expect(
        service.changePassword(mockUser.id, {
          oldPassword: 'old',
          newPassword: 'new',
        }),
      ).rejects.toThrow(BadRequestException);
      // 应该包含错误码 ORG-AUTH-1009
      
      expect(prisma.user.update).not.toHaveBeenCalled();
    });

    it('[测试场景 5.2.4] 旧密码错误应抛出异常', async () => {
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({
        ...mockUser,
        source: 'LOCAL',
        passwordHash: '$2b$10$oldhash',
      } as any);
      (bcrypt.compare as jest.Mock).mockResolvedValue(false);

      await expect(
        service.changePassword(mockUser.id, {
          oldPassword: 'wrong-password',
          newPassword: 'NewPassword123',
        }),
      ).rejects.toThrow(UnauthorizedException);
      // 应该包含错误码 ORG-AUTH-1001
      
      expect(prisma.user.update).not.toHaveBeenCalled();
    });

    it('[测试场景 5.2.5] 新密码不符合复杂度要求', async () => {
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({
        ...mockUser,
        source: 'LOCAL',
        passwordHash: '$2b$10$oldhash',
      } as any);
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);

      // 假设密码验证在 DTO 层或 Service 层进行
      await expect(
        service.changePassword(mockUser.id, {
          oldPassword: 'old123',
          newPassword: '123', // 太短，不符合要求
        }),
      ).rejects.toThrow(BadRequestException);
      // 应该包含错误码 ORG-AUTH-1002
      
      expect(prisma.user.update).not.toHaveBeenCalled();
    });

    it('用户不存在应抛出异常', async () => {
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      await expect(
        service.changePassword('non-existent', {
          oldPassword: 'old',
          newPassword: 'NewPassword123',
        }),
      ).rejects.toThrow(UnauthorizedException);
      
      expect(prisma.user.update).not.toHaveBeenCalled();
    });
  });

  describe('register', () => {
    it('应该成功注册新用户', async () => {
      const registerDto = {
        username: 'newuser',
        email: 'new@example.com',
        password: 'password123',
        displayName: 'New User',
      };

      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(null);
      (bcrypt.hash as jest.Mock).mockResolvedValue('$2b$10$hashedpassword');
      jest.spyOn(prisma.user, 'create').mockResolvedValue({
        id: 'new-user-id',
        ...registerDto,
        passwordHash: '$2b$10$hashedpassword',
        status: 'ACTIVE',
        source: 'LOCAL',
        createdAt: new Date(),
        updatedAt: new Date(),
      } as any);
      jest.spyOn(jwtService, 'sign').mockReturnValue('mock-token');

      const result = await service.register(registerDto);

      expect(result.user.username).toBe(registerDto.username);
      expect(result.token).toBeDefined();
      expect(prisma.user.create).toHaveBeenCalled();
    });

    it('用户名已存在应抛出异常', async () => {
      jest.spyOn(prisma.user, 'findFirst').mockResolvedValue(mockUser as any);

      await expect(
        service.register({
          username: 'testuser',
          email: 'new@example.com',
          password: 'password123',
          displayName: 'New User',
        }),
      ).rejects.toThrow(UnauthorizedException);
    });
  });
});
