import { Test } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UnauthorizedException, ConflictException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { AuthService } from './auth.service';
import { PrismaService } from '@chatapp/database';
import { TwoFactorService } from './two-factor.service';

jest.mock('bcryptjs');

describe('AuthService', () => {
  let service: AuthService;
  let prisma: any;
  let jwtService: any;
  let configService: any;

  beforeEach(async () => {
    prisma = {
      user: {
        findUnique: jest.fn(),
        create: jest.fn(),
      },
      workspace: {
        create: jest.fn(),
      },
      workspaceMember: {
        findMany: jest.fn(),
      },
      channel: {
        create: jest.fn(),
      },
      channelMember: {
        create: jest.fn(),
      },
      session: {
        create: jest.fn(),
        findFirst: jest.fn(),
        update: jest.fn(),
        updateMany: jest.fn(),
      },
      twoFactorAuth: {
        findUnique: jest.fn(),
      },
    };

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

    configService = {
      get: jest.fn((key: string, defaultVal?: any) => {
        const map: Record<string, any> = {
          JWT_SECRET: 'test-secret',
          JWT_ACCESS_EXPIRES: '15m',
          JWT_REFRESH_EXPIRES: '30d',
        };
        return map[key] ?? defaultVal;
      }),
    };

    const moduleRef = await Test.createTestingModule({
      providers: [
        AuthService,
        { provide: PrismaService, useValue: prisma },
        { provide: JwtService, useValue: jwtService },
        { provide: ConfigService, useValue: configService },
        { provide: TwoFactorService, useValue: { verifyToken: jest.fn(), generateSecret: jest.fn(), enable: jest.fn(), disable: jest.fn() } },
      ],
    }).compile();

    service = moduleRef.get<AuthService>(AuthService);

    // Default bcrypt mocks
    (bcrypt.hash as jest.Mock).mockResolvedValue('hashed-password');
    (bcrypt.compare as jest.Mock).mockResolvedValue(true);
  });

  describe('register()', () => {
    it('crea usuario y devuelve token', async () => {
      prisma.user.findUnique.mockResolvedValue(null);
      prisma.user.create.mockResolvedValue({
        id: 'user-1',
        email: 'test@test.com',
        displayName: 'Test User',
        avatarUrl: null,
      });
      jwtService.signAsync
        .mockResolvedValueOnce('access-token')
        .mockResolvedValueOnce('refresh-token');

      const result = await service.register({
        email: 'test@test.com',
        password: 'Password123',
        display_name: 'Test User',
      });

      expect(prisma.user.create).toHaveBeenCalled();
      expect(result.access_token).toBe('access-token');
      expect(result.refresh_token).toBe('refresh-token');
      expect((result as any).user.email).toBe('test@test.com');
    });

    it('lanza ConflictException si email ya existe', async () => {
      prisma.user.findUnique.mockResolvedValue({ id: 'existing' });

      await expect(
        service.register({
          email: 'test@test.com',
          password: 'Password123',
          display_name: 'Test User',
        }),
      ).rejects.toThrow(ConflictException);
    });
  });

  describe('login()', () => {
    it('con credenciales validas devuelve token', async () => {
      prisma.user.findUnique.mockResolvedValue({
        id: 'user-1',
        email: 'test@test.com',
        passwordHash: 'hashed-password',
        displayName: 'Test User',
        avatarUrl: null,
        isDeleted: false,
      });
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);
      prisma.twoFactorAuth.findUnique.mockResolvedValue(null);
      prisma.workspaceMember.findMany.mockResolvedValue([
        {
          workspace: { id: 'ws-1', name: 'Test', domain: 'test' },
          role: 'member',
        },
      ]);
      jwtService.signAsync
        .mockResolvedValueOnce('access-token')
        .mockResolvedValueOnce('refresh-token');

      const result = await service.login({
        email: 'test@test.com',
        password: 'Password123',
      });

      expect(result.access_token).toBe('access-token');
      expect(result.refresh_token).toBe('refresh-token');
      expect((result as any).user.email).toBe('test@test.com');
      expect((result as any).workspaces).toHaveLength(1);
    });

    it('con password incorrecto lanza UnauthorizedException', async () => {
      prisma.user.findUnique.mockResolvedValue({
        id: 'user-1',
        email: 'test@test.com',
        passwordHash: 'hashed-password',
        isDeleted: false,
      });
      (bcrypt.compare as jest.Mock).mockResolvedValue(false);

      await expect(
        service.login({ email: 'test@test.com', password: 'wrong' }),
      ).rejects.toThrow(UnauthorizedException);
    });

    it('con email inexistente lanza UnauthorizedException', async () => {
      prisma.user.findUnique.mockResolvedValue(null);

      await expect(
        service.login({ email: 'nobody@test.com', password: 'Password123' }),
      ).rejects.toThrow(UnauthorizedException);
    });
  });

  describe('refresh()', () => {
    it('con token valido devuelve nuevo token', async () => {
      jwtService.verifyAsync.mockResolvedValue({ userId: 'user-1', email: 'test@test.com' });
      prisma.session.findFirst.mockResolvedValue({ id: 'session-1' });
      prisma.user.findUnique.mockResolvedValue({
        id: 'user-1',
        email: 'test@test.com',
        isDeleted: false,
      });
      jwtService.signAsync
        .mockResolvedValueOnce('new-access-token')
        .mockResolvedValueOnce('new-refresh-token');

      const result = await service.refresh({ refresh_token: 'valid-refresh' });

      expect(result.accessToken).toBe('new-access-token');
      expect(result.refreshToken).toBe('new-refresh-token');
      expect(prisma.session.update).toHaveBeenCalledWith({
        where: { id: 'session-1' },
        data: { revokedAt: expect.any(Date) },
      });
    });

    it('con token invalido lanza UnauthorizedException', async () => {
      jwtService.verifyAsync.mockRejectedValue(new Error('invalid'));

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