import { type IRefreshTokenRepository, type RefreshTokenRecord } from '../../domain/repositories/refresh-token.repository';
import { TokenService } from '../services/token.service';

describe('TokenService', () => {
  let service: TokenService;
  let mockJwtService: { sign: ReturnType<typeof vi.fn>; verify: ReturnType<typeof vi.fn> };
  let mockRefreshTokenRepo: { [K in keyof IRefreshTokenRepository]: ReturnType<typeof vi.fn> };

  const payload = { sub: 'user-1', phone: '0912345678', role: 'BUYER' };

  beforeEach(() => {
    mockJwtService = {
      sign: vi.fn().mockReturnValue('signed-jwt'),
      verify: vi.fn(),
    };
    mockRefreshTokenRepo = {
      create: vi.fn().mockResolvedValue({} as RefreshTokenRecord),
      findByToken: vi.fn(),
      revokeByFamily: vi.fn().mockResolvedValue(undefined),
      revokeAllForUser: vi.fn().mockResolvedValue(undefined),
      deleteExpired: vi.fn(),
    };

    service = new TokenService(
      mockJwtService as any,
      mockRefreshTokenRepo as any,
    );
  });

  describe('generateTokenPair', () => {
    it('returns access token, refresh token with family prefix, and expiresIn', async () => {
      const result = await service.generateTokenPair(payload);

      expect(result.accessToken).toBe('signed-jwt');
      expect(result.refreshToken).toContain('.');
      expect(result.expiresIn).toBe(900);
      expect(mockJwtService.sign).toHaveBeenCalledWith(payload);
      expect(mockRefreshTokenRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          userId: 'user-1',
          revokedAt: null,
        }),
      );
    });

    it('creates refresh token record with 30-day expiry', async () => {
      await service.generateTokenPair(payload);

      const createCall = mockRefreshTokenRepo.create.mock.calls[0][0];
      const expiresAt = createCall.expiresAt as Date;
      const now = new Date();
      const daysDiff = Math.round((expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
      expect(daysDiff).toBeGreaterThanOrEqual(29);
      expect(daysDiff).toBeLessThanOrEqual(31);
    });
  });

  describe('rotateRefreshToken', () => {
    const makeExistingToken = (overrides?: Partial<RefreshTokenRecord>): RefreshTokenRecord => ({
      id: 'rt-1',
      userId: 'user-1',
      token: 'hashed-token',
      family: 'old-family',
      expiresAt: new Date(Date.now() + 86400000),
      revokedAt: null,
      createdAt: new Date(),
      ...overrides,
    });

    it('rotates valid token: revokes old family, creates new token', async () => {
      mockRefreshTokenRepo.findByToken.mockResolvedValue(makeExistingToken());
      mockRefreshTokenRepo.create.mockResolvedValue({} as RefreshTokenRecord);

      const result = await service.rotateRefreshToken('old-family.raw-token-hex');

      expect(result).not.toBeNull();
      expect(result!.userId).toBe('user-1');
      expect(result!.refreshToken).toContain('.');
      expect(mockRefreshTokenRepo.revokeByFamily).toHaveBeenCalledWith('old-family');
      expect(mockRefreshTokenRepo.create).toHaveBeenCalled();
    });

    it('returns null for malformed token (no dot separator)', async () => {
      const result = await service.rotateRefreshToken('no-dot-separator');
      expect(result).toBeNull();
    });

    it('returns null and revokes family when token not found (reuse attack)', async () => {
      mockRefreshTokenRepo.findByToken.mockResolvedValue(null);

      const result = await service.rotateRefreshToken('suspect-family.unknown-token');

      expect(result).toBeNull();
      expect(mockRefreshTokenRepo.revokeByFamily).toHaveBeenCalledWith('suspect-family');
    });

    it('returns null and revokes family when token is already revoked', async () => {
      mockRefreshTokenRepo.findByToken.mockResolvedValue(
        makeExistingToken({ revokedAt: new Date() }),
      );

      const result = await service.rotateRefreshToken('old-family.revoked-token');

      expect(result).toBeNull();
      expect(mockRefreshTokenRepo.revokeByFamily).toHaveBeenCalled();
    });

    it('returns null and revokes family when token is expired', async () => {
      mockRefreshTokenRepo.findByToken.mockResolvedValue(
        makeExistingToken({ expiresAt: new Date(Date.now() - 86400000) }),
      );

      const result = await service.rotateRefreshToken('old-family.expired-token');

      expect(result).toBeNull();
      expect(mockRefreshTokenRepo.revokeByFamily).toHaveBeenCalled();
    });

    it('returns null for empty family segment', async () => {
      const result = await service.rotateRefreshToken('.some-raw-token');
      expect(result).toBeNull();
    });

    it('returns null for empty raw token segment', async () => {
      const result = await service.rotateRefreshToken('some-family.');
      expect(result).toBeNull();
    });
  });

  describe('generateAccessToken', () => {
    it('delegates to jwtService.sign', () => {
      const token = service.generateAccessToken(payload);
      expect(token).toBe('signed-jwt');
      expect(mockJwtService.sign).toHaveBeenCalledWith(payload);
    });
  });

  describe('revokeAllUserTokens', () => {
    it('revokes all tokens for a user', async () => {
      await service.revokeAllUserTokens('user-1');
      expect(mockRefreshTokenRepo.revokeAllForUser).toHaveBeenCalledWith('user-1');
    });
  });

  describe('verifyAccessToken', () => {
    it('returns decoded payload for valid token', () => {
      mockJwtService.verify.mockReturnValue(payload);
      const result = service.verifyAccessToken('valid-jwt');
      expect(result).toEqual(payload);
    });

    it('returns null for invalid token', () => {
      mockJwtService.verify.mockImplementation(() => { throw new Error('invalid'); });
      const result = service.verifyAccessToken('bad-jwt');
      expect(result).toBeNull();
    });
  });
});
