import { TrackingEventType } from '../common/constants/enums';
import {
  computeLegCoverage,
  dedupeCoveragePoints,
  pointDedupeKey,
} from './tracking-coverage.util';

function minutesFrom(base: Date, minutes: number): Date {
  return new Date(base.getTime() + minutes * 60_000);
}

function pointsEverySeconds(
  start: Date,
  end: Date,
  intervalSec: number,
  lat = 12.9,
  lng = 77.5,
) {
  const out: Array<{ timestamp: Date; latitude: number; longitude: number }> =
    [];
  for (let t = start.getTime(); t <= end.getTime(); t += intervalSec * 1000) {
    out.push({ timestamp: new Date(t), latitude: lat, longitude: lng });
  }
  return out;
}

describe('tracking-coverage.util', () => {
  const departure = new Date('2026-06-16T09:00:00.000Z');
  const arrival = minutesFrom(departure, 50);
  const gapThresholdSeconds = 90;

  it('dedupes points with same timestamp+coordinates', () => {
    const ts = new Date('2026-06-16T09:00:00.000Z');
    const key = pointDedupeKey(ts, 12.9, 77.5);
    expect(key).toBe(pointDedupeKey(ts, 12.9, 77.5));
    const deduped = dedupeCoveragePoints([
      { timestamp: ts, latitude: 12.9, longitude: 77.5 },
      { timestamp: ts, latitude: 12.9, longitude: 77.5 },
      { timestamp: minutesFrom(ts, 1), latitude: 12.91, longitude: 77.51 },
    ]);
    expect(deduped).toHaveLength(2);
  });

  it('reports ~100% coverage for 50 min window with points every 6s', () => {
    const points = pointsEverySeconds(departure, arrival, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points,
      },
      gapThresholdSeconds,
    );

    expect(result.expectedDurationMinutes).toBe(50);
    expect(result.coveragePercent).toBeGreaterThanOrEqual(99);
    expect(result.gapDurationMinutes).toBeLessThanOrEqual(0.2);
    expect(result.pointCount).toBeGreaterThan(400);
  });

  it('detects a 10 minute hole in the middle', () => {
    const holeStart = minutesFrom(departure, 20);
    const holeEnd = minutesFrom(departure, 30);
    const before = pointsEverySeconds(departure, holeStart, 6);
    const after = pointsEverySeconds(holeEnd, arrival, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points: [...before, ...after],
      },
      gapThresholdSeconds,
    );

    expect(result.gapDurationMinutes).toBeGreaterThanOrEqual(9.5);
    expect(result.gapDurationMinutes).toBeLessThanOrEqual(10.5);
    expect(result.gaps.length).toBeGreaterThanOrEqual(1);
  });

  it('detects initial gap when first point is 5 minutes after departure', () => {
    const firstPoint = minutesFrom(departure, 5);
    const points = pointsEverySeconds(firstPoint, arrival, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points,
      },
      gapThresholdSeconds,
    );

    expect(result.gaps[0].durationMinutes).toBeGreaterThanOrEqual(4.9);
    expect(result.gaps[0].durationMinutes).toBeLessThanOrEqual(5.1);
    expect(result.gaps[0].reason).toBe('no_points');
  });

  it('detects final gap when last point is 3 minutes before arrival', () => {
    const lastPoint = minutesFrom(arrival, -3);
    const points = pointsEverySeconds(departure, lastPoint, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points,
      },
      gapThresholdSeconds,
    );

    const finalGap = result.gaps[result.gaps.length - 1];
    expect(finalGap.durationMinutes).toBeGreaterThanOrEqual(2.9);
    expect(finalGap.durationMinutes).toBeLessThanOrEqual(3.1);
  });

  it('returns 0% coverage when no points in window', () => {
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points: [],
      },
      gapThresholdSeconds,
    );

    expect(result.coveragePercent).toBe(0);
    expect(result.gapDurationMinutes).toBe(50);
    expect(result.trackedDurationMinutes).toBe(0);
  });

  it('correlates permission_denied event with gap suspectedCause', () => {
    const holeStart = minutesFrom(departure, 20);
    const holeEnd = minutesFrom(departure, 30);
    const before = pointsEverySeconds(departure, holeStart, 6);
    const after = pointsEverySeconds(holeEnd, arrival, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: arrival,
        points: [...before, ...after],
        events: [
          {
            type: TrackingEventType.PERMISSION_DENIED,
            timestamp: minutesFrom(departure, 25),
          },
        ],
      },
      gapThresholdSeconds,
    );

    expect(result.gaps.some((g) => g.suspectedCause === 'permission_denied')).toBe(
      true,
    );
  });

  it('uses now() for open leg without arrival', () => {
    const now = minutesFrom(departure, 10);
    const points = pointsEverySeconds(departure, now, 6);
    const result = computeLegCoverage(
      {
        legId: 'leg-1',
        legNumber: 1,
        departureAt: departure,
        arrivalAt: null,
        points,
      },
      gapThresholdSeconds,
      now,
    );

    expect(result.expectedDurationMinutes).toBe(10);
    expect(result.arrivalAt).toBeNull();
    expect(result.coveragePercent).toBeGreaterThan(90);
  });
});
