import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { getRepositoryToken } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { AuthService } from './auth.service';
import { UserEntity } from '../entities/user.entity';
import { TenantEntity } from '../entities/tenant.entity';
import { TenantSettingsEntity } from '../entities/tenant-settings.entity';
import { RolePermissionEntity } from '../entities/role-permission.entity';
import { RedisService } from '../redis/redis.service';

jest.mock('bcrypt');

describe('AuthService', () => {
  let service: AuthService;

  const mockUserRepo = {
    findOne: jest.fn(),
    update: jest.fn(),
  };

  const mockTenantRepo = {
    findOne: jest.fn(),
  };

  const mockSettingsRepo = {
    findOne: jest.fn(),
  };

  const mockRolePermRepo = {
    find: jest.fn(),
  };

  const mockJwtService = {
    sign: jest.fn(),
  };

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        { provide: getRepositoryToken(UserEntity), useValue: mockUserRepo },
        { provide: getRepositoryToken(TenantEntity), useValue: mockTenantRepo },
        { provide: getRepositoryToken(TenantSettingsEntity), useValue: mockSettingsRepo },
        { provide: getRepositoryToken(RolePermissionEntity), useValue: mockRolePermRepo },
        { provide: JwtService, useValue: mockJwtService },
        { provide: RedisService, useValue: mockRedisService },
      ],
    }).compile();

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

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

  describe('login', () => {
    const loginDto = { email: 'admin@test.com', password: 'Password123' };
    const tenantId = 'tenant-1';

    it('should return auth response on successful login', async () => {
      mockRedisService.get.mockResolvedValue(null); // no lockout
      mockUserRepo.findOne.mockResolvedValue({
        id: 'u1',
        name: 'Admin',
        email: 'admin@test.com',
        password_hash: 'hashed',
        is_active: true,
        tenant_id: tenantId,
        role_id: 'r1',
        role: { name: 'Admin' },
      });
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);
      mockJwtService.sign.mockReturnValue('jwt-token');
      mockTenantRepo.findOne.mockResolvedValue({ name: 'Test DMC' });
      mockSettingsRepo.findOne.mockResolvedValue({
        primary_color: '#10b981',
        secondary_color: '#1e293b',
        logo_url: null,
      });
      mockRolePermRepo.find.mockResolvedValue([
        { module: 'users', access_level: 'delete' },
      ]);
      mockRedisService.del.mockResolvedValue(undefined);
      mockUserRepo.update.mockResolvedValue(undefined);

      const result = await service.login(loginDto, tenantId);

      expect(result.access_token).toBe('jwt-token');
      expect(result.user.email).toBe('admin@test.com');
      expect(result.user.permissions).toHaveProperty('users', 'delete');
      expect(result.theme).toBeDefined();
    });

    it('should throw UnauthorizedException when user not found', async () => {
      mockRedisService.get.mockResolvedValue(null);
      mockUserRepo.findOne.mockResolvedValue(null);
      mockRedisService.set.mockResolvedValue(undefined);

      await expect(service.login(loginDto, tenantId)).rejects.toThrow(UnauthorizedException);
    });

    it('should throw UnauthorizedException when account is deactivated', async () => {
      mockRedisService.get.mockResolvedValue(null);
      mockUserRepo.findOne.mockResolvedValue({
        id: 'u1', is_active: false, password_hash: 'h',
      });

      await expect(service.login(loginDto, tenantId)).rejects.toThrow(UnauthorizedException);
    });

    it('should throw UnauthorizedException when password is invalid', async () => {
      mockRedisService.get.mockResolvedValue(null);
      mockUserRepo.findOne.mockResolvedValue({
        id: 'u1', is_active: true, password_hash: 'h',
      });
      (bcrypt.compare as jest.Mock).mockResolvedValue(false);
      mockRedisService.set.mockResolvedValue(undefined);

      await expect(service.login(loginDto, tenantId)).rejects.toThrow(UnauthorizedException);
    });

    it('should throw ForbiddenException when account is locked', async () => {
      const futureDate = new Date(Date.now() + 60000).toISOString();
      mockRedisService.get.mockResolvedValue(
        JSON.stringify({ attempts: 5, lockedUntil: futureDate }),
      );

      await expect(service.login(loginDto, tenantId)).rejects.toThrow(ForbiddenException);
    });

    it('should record failed attempt on wrong credentials', async () => {
      mockRedisService.get.mockResolvedValue(null);
      mockUserRepo.findOne.mockResolvedValue(null);
      mockRedisService.set.mockResolvedValue(undefined);

      await expect(service.login(loginDto, tenantId)).rejects.toThrow(UnauthorizedException);
      // recordFailedAttempt reads then writes
      expect(mockRedisService.set).toHaveBeenCalled();
    });
  });

  describe('validateUserFromToken', () => {
    it('should return user when valid token payload', async () => {
      const user = { id: 'u1', is_active: true };
      mockUserRepo.findOne.mockResolvedValue(user);

      const result = await service.validateUserFromToken({
        sub: 'u1', email: 'a@b.com', tenant_id: 't1',
      });

      expect(result).toEqual(user);
    });

    it('should return null when user not found', async () => {
      mockUserRepo.findOne.mockResolvedValue(null);

      const result = await service.validateUserFromToken({
        sub: 'u1', email: 'a@b.com', tenant_id: 't1',
      });

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

  describe('getProfile', () => {
    it('should return profile with permissions', async () => {
      mockUserRepo.findOne.mockResolvedValue({
        id: 'u1', name: 'Admin', email: 'admin@test.com', phone: null,
        tenant_id: 't1', role_id: 'r1', role: { name: 'Admin' },
      });
      mockRolePermRepo.find.mockResolvedValue([
        { module: 'users', access_level: 'delete' },
      ]);

      const result = await service.getProfile('u1', 't1');

      expect(result.id).toBe('u1');
      expect(result.permissions).toHaveProperty('users', 'delete');
    });

    it('should throw UnauthorizedException when user not found', async () => {
      mockUserRepo.findOne.mockResolvedValue(null);

      await expect(service.getProfile('u1', 't1')).rejects.toThrow(UnauthorizedException);
    });

    it('should return empty permissions when user has no role', async () => {
      mockUserRepo.findOne.mockResolvedValue({
        id: 'u1', name: 'Admin', email: 'admin@test.com', phone: null,
        tenant_id: 't1', role_id: null, role: null,
      });

      const result = await service.getProfile('u1', 't1');

      expect(result.permissions).toEqual({});
    });
  });
});
