import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
  parseOverpassElement,
  parseBbox,
  tileKeysForBbox,
  getAlprCameras,
  markTilesFetched,
} from '../../server/utils/alpr.js'
import { cameraToFeature, identifyingProperties, inferModelFromTags, isGenericCameraName } from '../../server/utils/alprGeo.js'
import { getDb, setDbPathForTest, closeDb } from '../../server/utils/db.js'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { randomUUID } from 'node:crypto'

describe('alpr utils', () => {
  describe('parseOverpassElement', () => {
    it('parses a valid OSM node', () => {
      const out = parseOverpassElement({
        type: 'node',
        id: 123,
        lat: 33.75,
        lon: -84.39,
        tags: { 'manufacturer': 'Flock Safety', 'camera:direction': '90' },
      })
      expect(out?.osmId).toBe(123)
      expect(out?.manufacturer).toBe('Flock Safety')
    })
  })

  describe('cameraToFeature', () => {
    it('returns GeoJSON Point feature with identifying tags', () => {
      const feature = cameraToFeature({
        osmId: 1,
        lat: 33.5,
        lng: -84.5,
        manufacturer: 'Flock Safety',
        direction: 90,
        fov: 45,
        tags: {
          'model': 'Falcon',
          'operator': 'City PD',
          'ref': 'CAM-12',
          'operator:wikidata': 'Q123',
          'fixme': 'verify mount',
        },
      })
      expect(feature.properties.manufacturer).toBe('Flock Safety')
      expect(feature.properties.model).toBe('Falcon')
      expect(feature.properties.operator).toBe('City PD')
      expect(feature.properties.ref).toBe('CAM-12')
      expect(feature.properties.operatorWikidata).toBe('Q123')
      expect(feature.properties.tags).toEqual({ fixme: 'verify mount' })
    })
  })

  describe('identifyingProperties', () => {
    it('omits brand when same as manufacturer', () => {
      const props = identifyingProperties({ manufacturer: 'Flock Safety', brand: 'Flock Safety', model: 'Falcon' })
      expect(props.manufacturer).toBe('Flock Safety')
      expect(props.model).toBe('Falcon')
      expect(props.brand).toBeUndefined()
    })

    it('infers model from lowercase name tag', () => {
      const props = identifyingProperties({
        manufacturer: 'Flock Safety',
        name: 'falcon',
      })
      expect(props.model).toBe('Falcon')
      expect(props.name).toBeUndefined()
      expect(props.modelUnknown).toBeUndefined()
    })

    it('drops generic camera names and flags unknown model', () => {
      const props = identifyingProperties({
        manufacturer: 'Flock Safety',
        name: 'Flock ALPR camera',
      })
      expect(props.name).toBeUndefined()
      expect(props.model).toBeUndefined()
      expect(props.modelUnknown).toBe(true)
    })
  })

  describe('inferModelFromTags', () => {
    it('reads model tag and mis-tagged Flock Falcon name', () => {
      expect(inferModelFromTags({ model: 'Sparrow' })).toBe('Sparrow')
      expect(inferModelFromTags({ name: 'Flock Falcon' })).toBe('Falcon')
      expect(isGenericCameraName('Flock ALPR camera')).toBe(true)
      expect(isGenericCameraName('Peachtree & 5th')).toBe(false)
    })
  })

  describe('parseBbox', () => {
    it('parses valid bbox', () => {
      expect(parseBbox({ south: 33.0, west: -85.0, north: 33.4, east: -84.6 })).toEqual({
        south: 33.0,
        west: -85.0,
        north: 33.4,
        east: -84.6,
      })
    })

    it('rejects oversized bbox', () => {
      expect(() => parseBbox({ south: 0, west: 0, north: 2, east: 1 })).toThrow(/bbox exceeds/)
    })
  })

  describe('cache and fetch', () => {
    let dbPath

    beforeEach(async () => {
      dbPath = join(tmpdir(), `kestrelos-alpr-${randomUUID()}.db`)
      setDbPathForTest(dbPath)
      await getDb()
    })

    afterEach(() => {
      closeDb()
      setDbPathForTest(null)
    })

    it('returns GeoJSON FeatureCollection from cache', async () => {
      const db = await getDb()
      const bbox = { south: 33.4, west: -85.0, north: 33.6, east: -84.0 }
      await db.run(
        `INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [99, 33.5, -84.5, 'Acme', 180, '{}', new Date().toISOString()],
      )
      await markTilesFetched(db, tileKeysForBbox(bbox))
      vi.stubGlobal('fetch', vi.fn())
      const result = await getAlprCameras(db, bbox)
      expect(result.type).toBe('FeatureCollection')
      expect(result.features).toHaveLength(1)
      expect(result.source).toBe('cache')
      vi.unstubAllGlobals()
    })

    it('falls back to cache when Overpass fails', async () => {
      const db = await getDb()
      await db.run(
        `INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [42, 33.5, -84.5, null, null, '{}', new Date().toISOString()],
      )
      vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
      const result = await getAlprCameras(db, { south: 33.4, west: -85.0, north: 33.6, east: -84.0 })
      expect(result.type).toBe('FeatureCollection')
      expect(result.features).toHaveLength(1)
      vi.unstubAllGlobals()
    })
  })
})
