import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import type { UserRecord } from './auth.service';
import { AuthService } from './auth.service';
import { PrismaService } from '../../prisma/prisma.service';

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

import * as bcrypt from 'bcrypt';

const mockPrisma = {
  $queryRawUnsafe: jest.fn(),
  $executeRawUnsafe: jest.fn(),
  resetTenantId: jest.fn(),
};

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

const mockConfigService = {
  get: jest.fn(),
};

const CLUB_ID = 'club-uuid-001';
const USER_ID = 'user-uuid-001';

const makeUser = (overrides: Partial<UserRecord> = {}): UserRecord => ({
  id: USER_ID,
  clubId: CLUB_ID,
  phone: '+79001234567',
  passwordHash: '$2b$10$hashedpassword',
  role: 'TRAINER',
  isActive: true,
  firstName: 'Ivan',
  lastName: 'Petrov',
  totpSecret: null,
  ...overrides,
});

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: JwtService, useValue: mockJwtService },
        { provide: ConfigService, useValue: mockConfigService },
      ],
    }).compile();

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

  // ---------------------------------------------------------------------------
  // validateUser
  // ---------------------------------------------------------------------------

  describe('validateUser', () => {
    it('returns user when credentials are valid', async () => {
      const user = makeUser();
      mockPrisma.$queryRawUnsafe.mockResolvedValue([user]);
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);

      const result = await service.validateUser('+79001234567', 'correctPassword');

      expect(result).toEqual(user);
      expect(mockPrisma.$queryRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('SELECT'),
        '+79001234567',
      );
      expect(bcrypt.compare).toHaveBeenCalledWith('correctPassword', user.passwordHash);
    });

    it('throws UnauthorizedException when phone is not found', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([]);

      await expect(service.validateUser('+79999999999', 'anyPassword')).rejects.toThrow(
        new UnauthorizedException('Invalid credentials'),
      );

      expect(bcrypt.compare).not.toHaveBeenCalled();
    });

    it('throws UnauthorizedException when account is deactivated', async () => {
      const user = makeUser({ isActive: false });
      mockPrisma.$queryRawUnsafe.mockResolvedValue([user]);

      await expect(service.validateUser('+79001234567', 'correctPassword')).rejects.toThrow(
        new UnauthorizedException('Account is deactivated'),
      );

      expect(bcrypt.compare).not.toHaveBeenCalled();
    });

    it('throws UnauthorizedException when password is wrong', async () => {
      const user = makeUser();
      mockPrisma.$queryRawUnsafe.mockResolvedValue([user]);
      (bcrypt.compare as jest.Mock).mockResolvedValue(false);

      await expect(service.validateUser('+79001234567', 'wrongPassword')).rejects.toThrow(
        new UnauthorizedException('Invalid credentials'),
      );

      expect(bcrypt.compare).toHaveBeenCalledWith('wrongPassword', user.passwordHash);
    });
  });

  // ---------------------------------------------------------------------------
  // login
  // ---------------------------------------------------------------------------

  describe('login', () => {
    it('generates tokens and stores refresh token', async () => {
      const user = makeUser();
      // getUserRoles query returns trainer role
      mockPrisma.$queryRawUnsafe.mockResolvedValueOnce([{ slug: 'trainer' }]);
      mockJwtService.signAsync
        .mockResolvedValueOnce('access-token-123')
        .mockResolvedValueOnce('refresh-token-456');
      mockConfigService.get.mockImplementation((key: string) => {
        if (key === 'jwt.refreshSecret') return 'refresh-secret';
        if (key === 'jwt.refreshExpiresIn') return '30d';
        return undefined;
      });
      mockPrisma.$executeRawUnsafe.mockResolvedValue(undefined);

      const result = await service.login(user);

      expect(result).toEqual({
        accessToken: 'access-token-123',
        refreshToken: 'refresh-token-456',
        user: {
          id: USER_ID,
          firstName: 'Ivan',
          lastName: 'Petrov',
          role: 'TRAINER',
          roles: ['TRAINER'],
          clubId: CLUB_ID,
        },
      });
      expect(mockJwtService.signAsync).toHaveBeenCalledTimes(2);
      expect(mockJwtService.signAsync).toHaveBeenCalledWith({
        sub: USER_ID,
        clubId: CLUB_ID,
        role: 'TRAINER',
        roles: ['TRAINER'],
      });
      expect(mockJwtService.signAsync).toHaveBeenCalledWith(
        { sub: USER_ID, clubId: CLUB_ID, role: 'TRAINER', roles: ['TRAINER'] },
        expect.objectContaining({
          secret: 'refresh-secret',
          expiresIn: '30d',
        }),
      );
      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO refresh_tokens'),
        USER_ID,
        'refresh-token-456',
        expect.any(String),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // refreshToken
  // ---------------------------------------------------------------------------

  describe('refreshToken', () => {
    it('returns new accessToken when refresh token is valid', async () => {
      const payload = { sub: USER_ID, clubId: CLUB_ID, role: 'trainer' };
      mockJwtService.verifyAsync.mockResolvedValue(payload);
      mockConfigService.get.mockReturnValue('refresh-secret');
      // First call: check refresh token exists; second call: getUserRoles
      mockPrisma.$queryRawUnsafe
        .mockResolvedValueOnce([{ id: 'token-uuid-001' }])
        .mockResolvedValueOnce([{ slug: 'trainer' }]);
      mockJwtService.signAsync.mockResolvedValue('new-access-token');

      const result = await service.refreshToken('valid-refresh-token');

      expect(result).toEqual({ accessToken: 'new-access-token' });
      expect(mockJwtService.verifyAsync).toHaveBeenCalledWith('valid-refresh-token', {
        secret: 'refresh-secret',
      });
      expect(mockJwtService.signAsync).toHaveBeenCalledWith({
        sub: USER_ID,
        clubId: CLUB_ID,
        role: 'trainer',
        roles: ['trainer'],
      });
    });

    it('throws UnauthorizedException when token verification fails', async () => {
      mockJwtService.verifyAsync.mockRejectedValue(new Error('jwt expired'));
      mockConfigService.get.mockReturnValue('refresh-secret');

      await expect(service.refreshToken('expired-token')).rejects.toThrow(
        new UnauthorizedException('Invalid or expired refresh token'),
      );

      expect(mockPrisma.$queryRawUnsafe).not.toHaveBeenCalled();
    });

    it('throws UnauthorizedException when refresh token is revoked or not found', async () => {
      const payload = { sub: USER_ID, clubId: CLUB_ID, role: 'trainer' };
      mockJwtService.verifyAsync.mockResolvedValue(payload);
      mockConfigService.get.mockReturnValue('refresh-secret');
      mockPrisma.$queryRawUnsafe.mockResolvedValue([]);

      await expect(service.refreshToken('revoked-token')).rejects.toThrow(
        new UnauthorizedException('Refresh token has been revoked or expired'),
      );

      expect(mockJwtService.signAsync).not.toHaveBeenCalled();
    });
  });
});
