import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { SessionService } from './session.service';
import { RefreshToken } from './entities/refresh-token.entity';

describe('SessionService', () => {
  let service: SessionService;
  let refreshTokenRepository: Repository<RefreshToken>;

  const mockRefreshToken = {
    id: 'token_123',
    userId: 'user_123',
    token: 'refresh_token_abc',
    deviceId: 'device_123',
    deviceInfo: {
      platform: 'ios',
      browser: 'Safari',
      os: 'iOS 17',
      ipAddress: '192.168.1.1',
    },
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    lastUsedAt: new Date(),
    createdAt: new Date(),
  };

  const mockRefreshTokenRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    count: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SessionService,
        {
          provide: getRepositoryToken(RefreshToken),
          useValue: mockRefreshTokenRepository,
        },
      ],
    }).compile();

    service = module.get<SessionService>(SessionService);
    refreshTokenRepository = module.get<Repository<RefreshToken>>(
      getRepositoryToken(RefreshToken),
    );

    // Reset mocks
    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getUserSessions', () => {
    it('should return all active sessions for a user', async () => {
      const sessions = [
        mockRefreshToken,
        { ...mockRefreshToken, id: 'token_456', deviceId: 'device_456' },
      ];
      mockRefreshTokenRepository.find.mockResolvedValue(sessions);

      const result = await service.getUserSessions('user_123');

      expect(result).toHaveLength(2);
      expect(result[0]).toHaveProperty('id');
      expect(result[0]).toHaveProperty('deviceInfo');
      expect(result[0]).toHaveProperty('lastUsedAt');
      expect(refreshTokenRepository.find).toHaveBeenCalledWith({
        where: { userId: 'user_123' },
        order: { lastUsedAt: 'DESC' },
      });
    });

    it('should return empty array if no sessions', async () => {
      mockRefreshTokenRepository.find.mockResolvedValue([]);

      const result = await service.getUserSessions('user_123');

      expect(result).toHaveLength(0);
    });

    it('should format session data correctly', async () => {
      mockRefreshTokenRepository.find.mockResolvedValue([mockRefreshToken]);

      const result = await service.getUserSessions('user_123');

      expect(result[0]).toEqual({
        id: mockRefreshToken.id,
        deviceId: mockRefreshToken.deviceId,
        deviceInfo: mockRefreshToken.deviceInfo,
        lastUsedAt: mockRefreshToken.lastUsedAt,
        createdAt: mockRefreshToken.createdAt,
        expiresAt: mockRefreshToken.expiresAt,
        isCurrent: false,
      });
    });

    it('should mark current session correctly', async () => {
      mockRefreshTokenRepository.find.mockResolvedValue([mockRefreshToken]);

      const result = await service.getUserSessions('user_123', 'token_123');

      expect(result[0].isCurrent).toBe(true);
    });
  });

  describe('revokeSession', () => {
    it('should revoke a specific session', async () => {
      mockRefreshTokenRepository.findOne.mockResolvedValue(mockRefreshToken);
      mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 1 });

      const result = await service.revokeSession('user_123', 'token_123');

      expect(result.success).toBe(true);
      expect(result.message).toContain('Session revoked successfully');
      expect(refreshTokenRepository.delete).toHaveBeenCalledWith({
        id: 'token_123',
      });
    });

    it('should throw NotFoundException if session not found', async () => {
      mockRefreshTokenRepository.findOne.mockResolvedValue(null);

      await expect(
        service.revokeSession('user_123', 'token_123'),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException if session belongs to another user', async () => {
      mockRefreshTokenRepository.findOne.mockResolvedValue({
        ...mockRefreshToken,
        userId: 'other_user',
      });

      await expect(
        service.revokeSession('user_123', 'token_123'),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('revokeAllSessions', () => {
    it('should revoke all sessions for a user', async () => {
      mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 3 });

      const result = await service.revokeAllSessions('user_123');

      expect(result.success).toBe(true);
      expect(result.sessionsRevoked).toBe(3);
      expect(result.message).toContain('3 sessions revoked successfully');
      expect(refreshTokenRepository.delete).toHaveBeenCalledWith({
        userId: 'user_123',
      });
    });

    it('should succeed even if no sessions to revoke', async () => {
      mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 0 });

      const result = await service.revokeAllSessions('user_123');

      expect(result.success).toBe(true);
      expect(result.sessionsRevoked).toBe(0);
    });

    it('should exclude current session if sessionId provided', async () => {
      mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 2 });

      const result = await service.revokeAllSessions('user_123', 'token_current');

      expect(result.success).toBe(true);
      expect(result.sessionsRevoked).toBe(2);
      // Should delete all except current
      expect(refreshTokenRepository.delete).toHaveBeenCalled();
    });
  });

  describe('getActiveSessionCount', () => {
    it('should return count of active sessions', async () => {
      mockRefreshTokenRepository.count.mockResolvedValue(5);

      const result = await service.getActiveSessionCount('user_123');

      expect(result).toBe(5);
      expect(refreshTokenRepository.count).toHaveBeenCalledWith({
        where: { userId: 'user_123' },
      });
    });

    it('should return 0 if no active sessions', async () => {
      mockRefreshTokenRepository.count.mockResolvedValue(0);

      const result = await service.getActiveSessionCount('user_123');

      expect(result).toBe(0);
    });
  });

  describe('isSessionValid', () => {
    it('should return true for valid session', async () => {
      mockRefreshTokenRepository.findOne.mockResolvedValue(mockRefreshToken);

      const result = await service.isSessionValid('token_123', 'user_123');

      expect(result).toBe(true);
      expect(refreshTokenRepository.findOne).toHaveBeenCalledWith({
        where: {
          id: 'token_123',
          userId: 'user_123',
        },
      });
    });

    it('should return false for non-existent session', async () => {
      mockRefreshTokenRepository.findOne.mockResolvedValue(null);

      const result = await service.isSessionValid('token_123', 'user_123');

      expect(result).toBe(false);
    });

    it('should return false for expired session', async () => {
      const expiredToken = {
        ...mockRefreshToken,
        expiresAt: new Date(Date.now() - 1000), // Expired 1 second ago
      };
      mockRefreshTokenRepository.findOne.mockResolvedValue(expiredToken);

      const result = await service.isSessionValid('token_123', 'user_123');

      expect(result).toBe(false);
    });
  });
});
