import { ConfigService } from '@nestjs/config';
import { ForbiddenException } from '@nestjs/common';
import { TripStatus } from '../common/constants/enums';
import { LiveTrackingService } from './live-tracking.service';
import type { JwtUserPayload } from '../auth/jwt.types';

describe('LiveTrackingService', () => {
  const actor: JwtUserPayload = {
    sub: 'u1',
    uid: 'u1',
    email: 'driver@test.com',
    role: 'EMPLOYEE' as never,
  };

  let service: LiveTrackingService;
  let trips: {
    findDocumentByTripId: jest.Mock;
    assertCanRead: jest.Mock;
  };
  let users: { findByUid: jest.Mock };
  let gps: { ingestLivePoint: jest.Mock };
  let config: ConfigService;

  beforeEach(() => {
    trips = {
      findDocumentByTripId: jest.fn(),
      assertCanRead: jest.fn(),
    };
    users = { findByUid: jest.fn() };
    gps = { ingestLivePoint: jest.fn() };
    config = {
      get: (key: string) => {
        if (key === 'LIVE_TRACKING_WS_ENABLED') return 'true';
        if (key === 'GPS_LIVE_MIN_INTERVAL_MS') return '2000';
        if (key === 'GPS_LIVE_MIN_DISTANCE_METERS') return '3';
        return undefined;
      },
    } as unknown as ConfigService;

    service = new LiveTrackingService(
      config,
      trips as never,
      users as never,
      gps as never,
    );
  });

  describe('shouldAcceptPoint (rate limit)', () => {
    it('accepts first point', () => {
      expect(
        service.shouldAcceptPoint('trip-1', 'user-1', 12.9, 77.5, new Date()),
      ).toEqual({ accept: true });
    });

    it('rejects duplicate within interval and distance', () => {
      const ts = new Date('2026-06-16T10:00:00.000Z');
      service.rememberAcceptedPoint('trip-1', 'user-1', 12.9, 77.5, ts);
      const soon = new Date(ts.getTime() + 1000);
      const result = service.shouldAcceptPoint(
        'trip-1',
        'user-1',
        12.90001,
        77.50001,
        soon,
      );
      expect(result.accept).toBe(false);
      expect(result.reason).toBe('rate_limited');
    });

    it('accepts when moved beyond threshold', () => {
      const ts = new Date('2026-06-16T10:00:00.000Z');
      service.rememberAcceptedPoint('trip-1', 'user-1', 12.9, 77.5, ts);
      const soon = new Date(ts.getTime() + 1000);
      const result = service.shouldAcceptPoint(
        'trip-1',
        'user-1',
        12.95,
        77.55,
        soon,
      );
      expect(result.accept).toBe(true);
    });
  });

  describe('authorizeEmit', () => {
    it('rejects non-owner', async () => {
      trips.findDocumentByTripId.mockResolvedValue({
        tripId: 'trip-1',
        userId: 'owner-id',
        status: TripStatus.TRAVELLING,
      });
      users.findByUid.mockResolvedValue({ _id: { toString: () => 'other-id' } });

      await expect(service.authorizeEmit(actor, 'trip-1')).rejects.toThrow(
        ForbiddenException,
      );
    });
  });

  describe('handleLocationUpdate', () => {
    beforeEach(() => {
      trips.findDocumentByTripId.mockResolvedValue({
        tripId: 'trip-1',
        userId: 'owner-id',
        status: TripStatus.TRAVELLING,
      });
      users.findByUid.mockResolvedValue({
        _id: { toString: () => 'owner-id' },
      });
      gps.ingestLivePoint.mockResolvedValue({
        persisted: true,
        latitude: 12.9,
        longitude: 77.5,
        timestamp: new Date('2026-06-16T10:00:00.000Z'),
        clientPointId: 'p1',
      });
    });

    it('broadcasts normalized payload when persisted', async () => {
      const result = await service.handleLocationUpdate(actor, {
        tripId: 'trip-1',
        requestId: 'trip-1',
        latitude: 12.9,
        longitude: 77.5,
        timestamp: '2026-06-16T10:00:00.000Z',
        speed: 10,
        heading: 90,
      });

      expect(result.persisted).toBe(true);
      expect(result.broadcast).toMatchObject({
        requestId: 'trip-1',
        tripId: 'trip-1',
        userId: 'owner-id',
        latitude: 12.9,
        longitude: 77.5,
        speed: 10,
        heading: 90,
      });
      expect(gps.ingestLivePoint).toHaveBeenCalled();
    });

    it('skips persist when rate limited', async () => {
      const ts = new Date('2026-06-16T10:00:00.000Z');
      service.rememberAcceptedPoint('trip-1', 'owner-id', 12.9, 77.5, ts);

      const result = await service.handleLocationUpdate(actor, {
        tripId: 'trip-1',
        latitude: 12.90001,
        longitude: 77.50001,
        timestamp: '2026-06-16T10:00:01.000Z',
      });

      expect(result.persisted).toBe(false);
      expect(result.reason).toBe('rate_limited');
      expect(gps.ingestLivePoint).not.toHaveBeenCalled();
    });
  });

  describe('authorizeJoin', () => {
    it('delegates to trips.assertCanRead', async () => {
      trips.assertCanRead.mockResolvedValue({ tripId: 'trip-1' });
      await service.authorizeJoin(actor, 'trip-1');
      expect(trips.assertCanRead).toHaveBeenCalledWith(actor, 'trip-1');
    });
  });
});
