/**
 * Unit tests for mapUtils.ts — pure logic, no leaflet, runs in jsdom.
 */

import { describe, it, expect } from 'vitest'
import {
  locationsToHeatPoints,
  pooToHeatPoints,
  locationsToMapPoints,
  pooToMapPoints,
  filterPooByTimeWindow,
  computeCenter,
  daysAgoISO,
} from './mapUtils'
import type { LocationRecord, PooRecord } from '../records'

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const loc1: LocationRecord = {
  person: 'alice',
  datetime: '2026-01-15T10:00:00Z',
  latitude: 39.9,
  longitude: 116.4,
  altitude: 50,
}
const loc2: LocationRecord = {
  person: 'alice',
  datetime: '2026-01-20T12:00:00Z',
  latitude: 39.95,
  longitude: 116.45,
  altitude: null,
}

const poo1: PooRecord = {
  timestamp: '2026-01-10T08:00:00Z',
  status: 'done',
  latitude: 39.91,
  longitude: 116.41,
}
const poo2: PooRecord = {
  timestamp: '2026-01-20T09:00:00Z',
  status: 'done',
  latitude: 39.92,
  longitude: 116.42,
}
const poo3: PooRecord = {
  timestamp: '2026-02-01T09:00:00Z',
  status: 'done',
  latitude: 39.93,
  longitude: 116.43,
}

// ---------------------------------------------------------------------------
// locationsToHeatPoints
// ---------------------------------------------------------------------------

describe('locationsToHeatPoints', () => {
  it('converts records to [lat, lng, 1] tuples', () => {
    const pts = locationsToHeatPoints([loc1, loc2])
    expect(pts).toHaveLength(2)
    expect(pts[0]).toEqual([39.9, 116.4, 1])
    expect(pts[1]).toEqual([39.95, 116.45, 1])
  })

  it('returns empty array for empty input', () => {
    expect(locationsToHeatPoints([])).toEqual([])
  })
})

// ---------------------------------------------------------------------------
// pooToHeatPoints
// ---------------------------------------------------------------------------

describe('pooToHeatPoints', () => {
  it('converts poo records to heat points', () => {
    const pts = pooToHeatPoints([poo1])
    expect(pts).toHaveLength(1)
    expect(pts[0]).toEqual([39.91, 116.41, 1])
  })
})

// ---------------------------------------------------------------------------
// locationsToMapPoints
// ---------------------------------------------------------------------------

describe('locationsToMapPoints', () => {
  it('attaches original record to each point', () => {
    const pts = locationsToMapPoints([loc1])
    expect(pts).toHaveLength(1)
    expect(pts[0].lat).toBe(39.9)
    expect(pts[0].lng).toBe(116.4)
    expect(pts[0].record).toBe(loc1)
  })
})

// ---------------------------------------------------------------------------
// pooToMapPoints
// ---------------------------------------------------------------------------

describe('pooToMapPoints', () => {
  it('attaches original poo record to each point', () => {
    const pts = pooToMapPoints([poo1])
    expect(pts[0].record).toBe(poo1)
  })
})

// ---------------------------------------------------------------------------
// filterPooByTimeWindow — client-side time filter
// ---------------------------------------------------------------------------

describe('filterPooByTimeWindow', () => {
  const records = [poo1, poo2, poo3]
  // timestamps: 2026-01-10, 2026-01-20, 2026-02-01

  it('returns all records when start and end are both null', () => {
    expect(filterPooByTimeWindow(records, null, null)).toHaveLength(3)
  })

  it('filters by start (inclusive)', () => {
    const result = filterPooByTimeWindow(records, '2026-01-15T00:00:00Z', null)
    expect(result).toHaveLength(2)
    expect(result.map((r) => r.timestamp)).toContain('2026-01-20T09:00:00Z')
    expect(result.map((r) => r.timestamp)).toContain('2026-02-01T09:00:00Z')
  })

  it('filters by end (inclusive)', () => {
    const result = filterPooByTimeWindow(records, null, '2026-01-20T09:00:00Z')
    expect(result).toHaveLength(2)
    expect(result.map((r) => r.timestamp)).toContain('2026-01-10T08:00:00Z')
    expect(result.map((r) => r.timestamp)).toContain('2026-01-20T09:00:00Z')
  })

  it('filters by both start and end', () => {
    const result = filterPooByTimeWindow(
      records,
      '2026-01-15T00:00:00Z',
      '2026-01-25T00:00:00Z',
    )
    expect(result).toHaveLength(1)
    expect(result[0].timestamp).toBe('2026-01-20T09:00:00Z')
  })

  it('returns empty when no records match', () => {
    const result = filterPooByTimeWindow(records, '2027-01-01T00:00:00Z', null)
    expect(result).toHaveLength(0)
  })

  it('includes records exactly at start boundary', () => {
    const result = filterPooByTimeWindow(records, '2026-01-10T08:00:00Z', null)
    expect(result.map((r) => r.timestamp)).toContain('2026-01-10T08:00:00Z')
  })

  it('includes records exactly at end boundary', () => {
    const result = filterPooByTimeWindow(records, null, '2026-02-01T09:00:00Z')
    expect(result.map((r) => r.timestamp)).toContain('2026-02-01T09:00:00Z')
  })
})

// ---------------------------------------------------------------------------
// computeCenter
// ---------------------------------------------------------------------------

describe('computeCenter', () => {
  it('returns null for empty array', () => {
    expect(computeCenter([])).toBeNull()
  })

  it('returns the point for a single-element array', () => {
    const result = computeCenter([{ lat: 10, lng: 20 }])
    expect(result).toEqual([10, 20])
  })

  it('returns the average of multiple points', () => {
    const result = computeCenter([
      { lat: 0, lng: 0 },
      { lat: 4, lng: 6 },
    ])
    expect(result).toEqual([2, 3])
  })
})

// ---------------------------------------------------------------------------
// daysAgoISO
// ---------------------------------------------------------------------------

describe('daysAgoISO', () => {
  it('returns a valid ISO string in the past', () => {
    const result = daysAgoISO(7)
    expect(typeof result).toBe('string')
    const d = new Date(result)
    expect(isNaN(d.getTime())).toBe(false)
    expect(d.getTime()).toBeLessThan(Date.now())
  })
})
