/**
 * ScatterLayer unit test — M2-T09 REWORK 1.
 *
 * This test exercises the REAL ScatterLayer code path (not a wholesale RecordsMap mock).
 * It verifies that ScatterLayer uses the imported leaflet namespace (L.markerClusterGroup)
 * rather than window.L / globalThis.L, which would silently fail in Vite ESM bundles.
 *
 * The test:
 *  - mocks react-leaflet's useMap() to return a fake map object
 *  - provides a mock markerClusterGroup spy via the leaflet module mock
 *  - renders ScatterLayer with some points
 *  - asserts that L.markerClusterGroup was called (i.e. the import path is used)
 *  - asserts that addLayer was called for each point
 *  - asserts that clicking a marker invokes onSelectLocation / onSelectPoo
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render } from '@testing-library/react'
import type { ReactNode } from 'react'

// ---------------------------------------------------------------------------
// Use vi.hoisted() to define mocks that are referenced inside vi.mock factories.
// vi.mock() factories are hoisted to the top of the file, so any variables they
// reference must also be hoisted.
// ---------------------------------------------------------------------------

const { markerClusterGroupSpy, fakeAddLayer, fakeMapAddLayer, markerClickHandlers } =
  vi.hoisted(() => {
    const clickHandlers: Array<() => void> = []
    const fakeAddLayer = vi.fn()
    const fakeCluster = {
      addLayer: fakeAddLayer,
      addTo: vi.fn(),
      clearLayers: vi.fn(),
    }
    const markerClusterGroupSpy = vi.fn(() => fakeCluster)
    const fakeMapAddLayer = vi.fn()
    return { markerClusterGroupSpy, fakeAddLayer, fakeMapAddLayer, markerClickHandlers: clickHandlers }
  })

// ---------------------------------------------------------------------------
// Mock leaflet BEFORE importing ScatterLayer.
// We use the hoisted spy so vi.mock factory can reference it safely.
// ---------------------------------------------------------------------------

vi.mock('leaflet', () => {
  const markerClusterGroupSpy_ = markerClusterGroupSpy
  const markerClickHandlers_ = markerClickHandlers

  // Icon must be a real constructor (used as `new Icon(...)`)
  class FakeIcon {
    constructor(_opts: unknown) {}
    static Default = { prototype: {}, mergeOptions: vi.fn() }
  }

  return {
    Icon: FakeIcon,
    DivIcon: vi.fn(function FakeDivIcon(_opts: unknown) { return {} }),
    heatLayer: vi.fn(() => ({ setLatLngs: vi.fn(), addTo: vi.fn() })),
    markerClusterGroup: markerClusterGroupSpy_,
    marker: vi.fn((_latlng: unknown, _opts: unknown) => {
      return {
        bindTooltip: vi.fn().mockReturnThis(),
        on: vi.fn((event: string, handler: () => void) => {
          if (event === 'click') {
            markerClickHandlers_.push(handler)
          }
          return { bindTooltip: vi.fn().mockReturnThis(), on: vi.fn().mockReturnThis() }
        }),
      }
    }),
    // `import * as L from 'leaflet'` in RecordsMap.tsx resolves to this module.
    // Vitest's module mock exposes all named exports as the namespace object,
    // so markerClusterGroup at the top level IS accessible as L.markerClusterGroup.
    default: {
      markerClusterGroup: markerClusterGroupSpy_,
    },
  }
})

vi.mock('leaflet.heat', () => ({}))
vi.mock('leaflet.markercluster', () => ({}))

// Mock image imports
vi.mock('leaflet/dist/images/marker-icon-2x.png', () => ({ default: 'marker-icon-2x.png' }))
vi.mock('leaflet/dist/images/marker-icon.png', () => ({ default: 'marker-icon.png' }))
vi.mock('leaflet/dist/images/marker-shadow.png', () => ({ default: 'marker-shadow.png' }))

// Mock CSS imports
vi.mock('leaflet/dist/leaflet.css', () => ({}))
vi.mock('leaflet.markercluster/dist/MarkerCluster.css', () => ({}))
vi.mock('leaflet.markercluster/dist/MarkerCluster.Default.css', () => ({}))

// ---------------------------------------------------------------------------
// Mock react-leaflet: MapContainer renders children, useMap returns fake map.
// ---------------------------------------------------------------------------

vi.mock('react-leaflet', () => ({
  MapContainer: ({ children }: { children: ReactNode }) => (
    <div data-testid="map-container">{children}</div>
  ),
  TileLayer: () => null,
  useMap: () => ({
    addLayer: fakeMapAddLayer,
    removeLayer: vi.fn(),
    hasLayer: vi.fn(() => false),
  }),
}))

// ---------------------------------------------------------------------------
// Import ScatterLayer AFTER mocks are set up.
// ---------------------------------------------------------------------------

import { ScatterLayer } from './RecordsMap'
import type { LocationMapPoint, PooMapPoint } from './mapUtils'
import type { LocationRecord, PooRecord } from '../records'

// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------

const locationRecord: LocationRecord = {
  person: 'alice',
  datetime: '2026-01-15T10:00:00Z',
  latitude: 39.9,
  longitude: 116.4,
  altitude: null,
}
const locationPoints: LocationMapPoint[] = [
  { lat: 39.9, lng: 116.4, record: locationRecord },
]

const pooRecord: PooRecord = {
  timestamp: '2026-01-20T09:00:00Z',
  status: 'done',
  latitude: 39.91,
  longitude: 116.41,
}
const pooPoints: PooMapPoint[] = [
  { lat: 39.91, lng: 116.41, record: pooRecord },
]

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('ScatterLayer (real code path — not mocked RecordsMap)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    markerClickHandlers.length = 0
  })

  it('calls L.markerClusterGroup (imported namespace) when showScatter=true', () => {
    render(
      <ScatterLayer
        locationScatterPoints={locationPoints}
        pooScatterPoints={[]}
        showScatter={true}
        onSelectLocation={vi.fn()}
        onSelectPoo={vi.fn()}
      />,
    )

    // KEY assertion: markerClusterGroup was called via the IMPORTED namespace.
    // With the old window.L / globalThis.L approach, this spy would never be
    // invoked because window.L is undefined in Vite ESM bundles.
    expect(markerClusterGroupSpy).toHaveBeenCalledOnce()
    expect(markerClusterGroupSpy).toHaveBeenCalledWith({
      maxClusterRadius: 50,
      showCoverageOnHover: false,
    })
  })

  it('calls cluster group addLayer for each location and poo scatter point', () => {
    render(
      <ScatterLayer
        locationScatterPoints={locationPoints}
        pooScatterPoints={pooPoints}
        showScatter={true}
        onSelectLocation={vi.fn()}
        onSelectPoo={vi.fn()}
      />,
    )

    // One addLayer call per point (1 location + 1 poo = 2).
    expect(fakeAddLayer).toHaveBeenCalledTimes(2)
    // The cluster group itself must be added to the map.
    const fakeCluster = markerClusterGroupSpy.mock.results[0]?.value
    expect(fakeMapAddLayer).toHaveBeenCalledWith(fakeCluster)
  })

  it('does NOT create cluster group when showScatter=false', () => {
    render(
      <ScatterLayer
        locationScatterPoints={locationPoints}
        pooScatterPoints={pooPoints}
        showScatter={false}
        onSelectLocation={vi.fn()}
        onSelectPoo={vi.fn()}
      />,
    )

    expect(markerClusterGroupSpy).not.toHaveBeenCalled()
    expect(fakeAddLayer).not.toHaveBeenCalled()
  })

  it('invokes onSelectLocation when a location marker is clicked', () => {
    const onSelectLocation = vi.fn()

    render(
      <ScatterLayer
        locationScatterPoints={locationPoints}
        pooScatterPoints={[]}
        showScatter={true}
        onSelectLocation={onSelectLocation}
        onSelectPoo={vi.fn()}
      />,
    )

    // At least one marker click handler should have been registered.
    expect(markerClickHandlers.length).toBeGreaterThan(0)
    // Simulate click on the first (location) marker.
    markerClickHandlers[0]()
    expect(onSelectLocation).toHaveBeenCalledOnce()
    expect(onSelectLocation).toHaveBeenCalledWith(locationRecord)
  })

  it('invokes onSelectPoo when a poo marker is clicked', () => {
    const onSelectPoo = vi.fn()

    render(
      <ScatterLayer
        locationScatterPoints={[]}
        pooScatterPoints={pooPoints}
        showScatter={true}
        onSelectLocation={vi.fn()}
        onSelectPoo={onSelectPoo}
      />,
    )

    expect(markerClickHandlers.length).toBeGreaterThan(0)
    markerClickHandlers[0]()
    expect(onSelectPoo).toHaveBeenCalledOnce()
    expect(onSelectPoo).toHaveBeenCalledWith(pooRecord)
  })
})
