/**
 * 管理员核心服务测试
 * 
 * 功能描述：
 * - 测试管理员登录认证功能
 * - 测试Token生成和验证功能
 * - 测试密码重置功能
 * - 测试管理员引导创建功能
 * 
 * 职责分离：
 * - 单元测试覆盖所有公共方法
 * - Mock外部依赖确保测试独立性
 * - 验证正常、异常、边界情况
 * 
 * 最近修改：
 * - 2026-01-07: 代码规范优化 - 添加完整的文件头注释
 * 
 * @author jianuo
 * @version 1.0.1
 * @since 2025-12-19
 * @lastModified 2026-01-07
 */

import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import { AdminAuthPayload, AdminCoreService } from './admin_core.service';
import { Users } from '../db/users/users.entity';

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

type UsersServiceLike = {
  findByUsername: jest.Mock;
  findByEmail: jest.Mock;
  findAll: jest.Mock;
  update: jest.Mock;
  create: jest.Mock;
};

describe('AdminCoreService', () => {
  let configService: Pick<ConfigService, 'get'>;
  let usersService: UsersServiceLike;
  let service: AdminCoreService;

  const secret = '0123456789abcdef';

  const signToken = (payload: AdminAuthPayload, tokenSecret: string): string => {
    const payloadJson = JSON.stringify(payload);
    const payloadPart = Buffer.from(payloadJson, 'utf8')
      .toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/g, '');

    const signature = crypto
      .createHmac('sha256', tokenSecret)
      .update(payloadPart)
      .digest('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/g, '');

    return `${payloadPart}.${signature}`;
  };

  beforeEach(() => {
    jest.restoreAllMocks();

    configService = {
      get: jest.fn((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        if (key === 'ADMIN_TOKEN_TTL_SECONDS') return defaultValue ?? '28800';
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'false';
        return defaultValue;
      }),
    };

    usersService = {
      findByUsername: jest.fn(),
      findByEmail: jest.fn(),
      findAll: jest.fn(),
      update: jest.fn(),
      create: jest.fn(),
    };

    service = new AdminCoreService(configService as ConfigService, usersService as any);
  });

  describe('login', () => {
    it('should reject when admin does not exist', async () => {
      usersService.findByUsername.mockResolvedValue(null);

      await expect(service.login({ identifier: 'admin', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException);
    });

    it('should reject non-admin user', async () => {
      usersService.findByUsername.mockResolvedValue({
        id: BigInt(1),
        username: 'user',
        nickname: 'U',
        role: 1,
        password_hash: 'hash',
      } as unknown as Users);

      await expect(service.login({ identifier: 'user', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException);
    });

    it('should reject admin without password_hash', async () => {
      usersService.findByUsername.mockResolvedValue({
        id: BigInt(1),
        username: 'admin',
        nickname: '管理员',
        role: 9,
        password_hash: null,
      } as unknown as Users);

      await expect(service.login({ identifier: 'admin', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException);
    });

    it('should reject wrong password', async () => {
      usersService.findByUsername.mockResolvedValue({
        id: BigInt(1),
        username: 'admin',
        nickname: '管理员',
        role: 9,
        password_hash: 'hash',
      } as unknown as Users);

      const bcryptAny = bcrypt as any;
      bcryptAny.compare.mockResolvedValue(false);

      await expect(service.login({ identifier: 'admin', password: 'bad' })).rejects.toBeInstanceOf(UnauthorizedException);
    });

    it('should login with valid credentials and generate verifiable token', async () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      usersService.findByUsername.mockResolvedValue({
        id: BigInt(1),
        username: 'admin',
        nickname: '管理员',
        role: 9,
        password_hash: 'hash',
      } as unknown as Users);

      const bcryptAny = bcrypt as any;
      bcryptAny.compare.mockResolvedValue(true);

      const result = await service.login({ identifier: 'admin', password: 'Admin123456' });

      expect(result.admin).toEqual({ id: '1', username: 'admin', nickname: '管理员', role: 9 });
      expect(result.access_token).toContain('.');
      expect(result.expires_at).toBeGreaterThan(now);

      const payload = service.verifyToken(result.access_token);
      expect(payload).toMatchObject({ adminId: '1', username: 'admin', role: 9 });
      expect(payload.iat).toBe(now);
      expect(payload.exp).toBe(result.expires_at);
    });

    it('should find admin by email identifier', async () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      usersService.findByUsername.mockResolvedValue(null);
      usersService.findByEmail.mockResolvedValue({
        id: BigInt(1),
        username: 'admin',
        nickname: '管理员',
        role: 9,
        password_hash: 'hash',
      } as unknown as Users);

      const bcryptAny = bcrypt as any;
      bcryptAny.compare.mockResolvedValue(true);

      const result = await service.login({ identifier: 'admin@test.com', password: 'Admin123456' });
      expect(result.admin.role).toBe(9);
    });

    it('should find admin by phone identifier', async () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      usersService.findByUsername.mockResolvedValue(null);
      usersService.findByEmail.mockResolvedValue(null);
      usersService.findAll.mockResolvedValue([
        {
          id: BigInt(2),
          username: 'admin',
          nickname: '管理员',
          role: 9,
          phone: '+86 13800000000',
          password_hash: 'hash',
        } as unknown as Users,
      ]);

      const bcryptAny = bcrypt as any;
      bcryptAny.compare.mockResolvedValue(true);

      const result = await service.login({ identifier: '+86 13800000000', password: 'Admin123456' });
      expect(result.admin.id).toBe('2');
    });

    it('should return phone-matched user via findUserByIdentifier (coverage for line 168)', async () => {
      usersService.findByUsername.mockResolvedValue(null);
      usersService.findByEmail.mockResolvedValue(null);
      usersService.findAll.mockResolvedValue([
        {
          id: BigInt(10),
          username: 'admin',
          nickname: '管理员',
          role: 9,
          phone: '13800000000',
        } as unknown as Users,
      ]);

      const found = await (service as any).findUserByIdentifier('13800000000');
      expect(found?.id?.toString()).toBe('10');
    });

    it('should fallback to default TTL when ADMIN_TOKEN_TTL_SECONDS is invalid', async () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        if (key === 'ADMIN_TOKEN_TTL_SECONDS') return 'not-a-number';
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'false';
        return defaultValue;
      });

      usersService.findByUsername.mockResolvedValue({
        id: BigInt(1),
        username: 'admin',
        nickname: '管理员',
        role: 9,
        password_hash: 'hash',
      } as unknown as Users);

      const bcryptAny = bcrypt as any;
      bcryptAny.compare.mockResolvedValue(true);

      const result = await service.login({ identifier: 'admin', password: 'Admin123456' });
      expect(result.expires_at).toBe(now + 28800 * 1000);
    });
  });

  describe('verifyToken', () => {
    it('should reject token when secret missing/too short', () => {
      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_TOKEN_SECRET') return 'short';
        return defaultValue;
      });

      expect(() => service.verifyToken('a.b')).toThrow(BadRequestException);
    });

    it('should reject invalid token format', () => {
      expect(() => service.verifyToken('no-dot')).toThrow(UnauthorizedException);
    });

    it('should reject token when payload JSON cannot be parsed (but signature valid)', () => {
      const payloadPart = Buffer.from('not-json', 'utf8')
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/g, '');

      const signature = crypto
        .createHmac('sha256', secret)
        .update(payloadPart)
        .digest('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/g, '');

      expect(() => service.verifyToken(`${payloadPart}.${signature}`)).toThrow(UnauthorizedException);
    });

    it('should accept valid signed token and return payload', () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      const payload: AdminAuthPayload = {
        adminId: '1',
        username: 'admin',
        role: 9,
        iat: now,
        exp: now + 1000,
      };

      const token = signToken(payload, secret);
      expect(service.verifyToken(token)).toEqual(payload);
    });

    it('should reject expired token', () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      const payload: AdminAuthPayload = {
        adminId: '1',
        username: 'admin',
        role: 9,
        iat: now - 1000,
        exp: now - 1,
      };

      const token = signToken(payload, secret);
      expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
    });

    it('should reject token with invalid signature', () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      const payload: AdminAuthPayload = {
        adminId: '1',
        username: 'admin',
        role: 9,
        iat: now,
        exp: now + 1000,
      };

      const token = signToken(payload, 'different_secret_012345');
      expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
    });

    it('should reject token with non-admin role', () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      const payload: AdminAuthPayload = {
        adminId: '1',
        username: 'user',
        role: 1,
        iat: now,
        exp: now + 1000,
      };

      const token = signToken(payload, secret);
      expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
    });

    it('should reject token when signature length mismatches expected', () => {
      const now = 1735689600000;
      jest.spyOn(Date, 'now').mockReturnValue(now);

      const payload: AdminAuthPayload = {
        adminId: '1',
        username: 'admin',
        role: 9,
        iat: now,
        exp: now + 1000,
      };

      // Valid payloadPart, but deliberately wrong signature length
      const payloadJson = JSON.stringify(payload);
      const payloadPart = Buffer.from(payloadJson, 'utf8')
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/g, '');

      expect(() => service.verifyToken(`${payloadPart}.x`)).toThrow(UnauthorizedException);
    });
  });

  describe('resetUserPassword', () => {
    it('should update user password_hash when password is strong', async () => {
      const bcryptAny = bcrypt as any;
      bcryptAny.hash.mockResolvedValue('hashed');
      usersService.update.mockResolvedValue({} as any);

      await service.resetUserPassword(BigInt(5), 'NewPass1234');

      expect(usersService.update).toHaveBeenCalledWith(BigInt(5), { password_hash: 'hashed' });
    });

    it('should reject weak password', async () => {
      await expect(service.resetUserPassword(BigInt(5), 'short')).rejects.toBeInstanceOf(BadRequestException);
    });

    it('should validate password strength directly (letters + numbers, 8+)', () => {
      expect(() => (service as any).validatePasswordStrength('12345678')).toThrow(BadRequestException);
      expect(() => (service as any).validatePasswordStrength('abcdefgh')).toThrow(BadRequestException);
      expect(() => (service as any).validatePasswordStrength('Abcdef12')).not.toThrow();
    });

    it('should reject too-long password (>128)', () => {
      const long = `Abc1${'x'.repeat(200)}`;
      expect(() => (service as any).validatePasswordStrength(long)).toThrow(BadRequestException);
    });
  });

  describe('bootstrapAdminIfEnabled', () => {
    it('should do nothing when bootstrap disabled', async () => {
      await service.onModuleInit();
      expect(usersService.findByUsername).not.toHaveBeenCalled();
      expect(usersService.create).not.toHaveBeenCalled();
    });

    it('should skip when enabled but missing username/password', async () => {
      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true';
        if (key === 'ADMIN_USERNAME') return undefined;
        if (key === 'ADMIN_PASSWORD') return undefined;
        if (key === 'ADMIN_NICKNAME') return defaultValue ?? '管理员';
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        return defaultValue;
      });

      await service.onModuleInit();

      expect(usersService.findByUsername).not.toHaveBeenCalled();
      expect(usersService.create).not.toHaveBeenCalled();
    });

    it('should skip when existing user already present', async () => {
      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true';
        if (key === 'ADMIN_USERNAME') return 'admin';
        if (key === 'ADMIN_PASSWORD') return 'Admin123456';
        if (key === 'ADMIN_NICKNAME') return '管理员';
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        return defaultValue;
      });

      usersService.findByUsername.mockResolvedValue({ id: BigInt(1), username: 'admin', role: 9 } as any);

      await service.onModuleInit();

      expect(usersService.create).not.toHaveBeenCalled();
    });

    it('should skip and warn when existing user has same username but non-admin role', async () => {
      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true';
        if (key === 'ADMIN_USERNAME') return 'admin';
        if (key === 'ADMIN_PASSWORD') return 'Admin123456';
        if (key === 'ADMIN_NICKNAME') return '管理员';
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        return defaultValue;
      });

      usersService.findByUsername.mockResolvedValue({ id: BigInt(1), username: 'admin', role: 1 } as any);

      await service.onModuleInit();
      expect(usersService.create).not.toHaveBeenCalled();
    });

    it('should create admin user when enabled and not existing', async () => {
      (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => {
        if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true';
        if (key === 'ADMIN_USERNAME') return 'admin';
        if (key === 'ADMIN_PASSWORD') return 'Admin123456';
        if (key === 'ADMIN_NICKNAME') return '管理员';
        if (key === 'ADMIN_TOKEN_SECRET') return secret;
        return defaultValue;
      });

      usersService.findByUsername.mockResolvedValue(null);
      const bcryptAny = bcrypt as any;
      bcryptAny.hash.mockResolvedValue('hashed');

      await service.onModuleInit();

      expect(usersService.create).toHaveBeenCalledWith({
        username: 'admin',
        password_hash: 'hashed',
        nickname: '管理员',
        role: 9,
        email_verified: true,
      });
    });
  });
});
