import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { rowToDevice, sanitizeDeviceForResponse, validateDeviceBody, DEVICE_TYPES, SOURCE_TYPES } from '../../server/utils/deviceUtils.js'

describe('deviceUtils', () => {
  describe('rowToDevice', () => {
    it('returns device row for valid db row', () => {
      const row = {
        id: 'd1',
        name: 'Cam 1',
        device_type: 'feed',
        vendor: null,
        lat: 37.7,
        lng: -122.4,
        stream_url: 'https://example.com/mjpeg',
        source_type: 'mjpeg',
        config: null,
      }
      expect(rowToDevice(row)).toEqual({ ...row, lat: 37.7, lng: -122.4 })
    })

    it('normalizes source_type to mjpeg when invalid', () => {
      const row = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: '', source_type: 'invalid', config: null }
      expect(rowToDevice(row)?.source_type).toBe('mjpeg')
    })

    it('accepts hls source_type', () => {
      const row = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: '', source_type: 'hls', config: null }
      expect(rowToDevice(row)?.source_type).toBe('hls')
    })

    it('returns null for null row', () => {
      expect(rowToDevice(null)).toBe(null)
    })

    it('returns null for row missing id', () => {
      expect(rowToDevice({ name: 'x', device_type: 'feed', lat: 0, lng: 0 })).toBe(null)
    })

    it('returns null for invalid lat/lng', () => {
      expect(rowToDevice({ id: 'd1', name: 'x', device_type: 'feed', lat: Number.NaN, lng: 0, stream_url: '', source_type: 'mjpeg' })).toBe(null)
    })

    it('coerces string lat/lng to numbers', () => {
      const row = { id: 'd1', name: 'x', device_type: 'feed', lat: '37.5', lng: '-122.0', stream_url: '', source_type: 'mjpeg', config: null }
      const out = rowToDevice(row)
      expect(out?.lat).toBe(37.5)
      expect(out?.lng).toBe(-122)
    })

    it('coerces non-string vendor, stream_url, config to null or empty', () => {
      const row = {
        id: 'd1',
        name: 'x',
        device_type: 'feed',
        vendor: 123,
        lat: 0,
        lng: 0,
        stream_url: null,
        source_type: 'mjpeg',
        config: 456,
      }
      const out = rowToDevice(row)
      expect(out?.vendor).toBe(null)
      expect(out?.stream_url).toBe('')
      expect(out?.config).toBe(null)
    })
  })

  describe('sanitizeDeviceForResponse', () => {
    it('returns camelCase streamUrl and sourceType', () => {
      const device = {
        id: 'd1',
        name: 'Cam',
        device_type: 'traffic',
        vendor: null,
        lat: 1,
        lng: 2,
        stream_url: 'https://example.com/stream',
        source_type: 'mjpeg',
        config: null,
      }
      const out = sanitizeDeviceForResponse(device)
      expect(out.streamUrl).toBe('https://example.com/stream')
      expect(out.sourceType).toBe('mjpeg')
      expect(out).not.toHaveProperty('stream_url')
    })

    it('sanitizes stream_url to empty for javascript:', () => {
      const device = {
        id: 'd1',
        name: 'x',
        device_type: 'feed',
        vendor: null,
        lat: 0,
        lng: 0,
        stream_url: 'javascript:alert(1)',
        source_type: 'mjpeg',
        config: null,
      }
      expect(sanitizeDeviceForResponse(device).streamUrl).toBe('')
    })

    it('sanitizes stream_url to empty when not http(s)', () => {
      const device = {
        id: 'd1',
        name: 'x',
        device_type: 'feed',
        vendor: null,
        lat: 0,
        lng: 0,
        stream_url: 'ftp://example.com',
        source_type: 'mjpeg',
        config: null,
      }
      expect(sanitizeDeviceForResponse(device).streamUrl).toBe('')
    })
  })

  describe('validateDeviceBody', () => {
    beforeEach(() => {
      vi.stubGlobal('createError', (opts) => {
        const err = new Error(opts?.message || 'Error')
        err.statusCode = opts?.statusCode
        throw err
      })
    })

    afterEach(() => {
      vi.unstubAllGlobals()
    })

    it('returns normalized body for valid input', () => {
      const body = {
        name: ' My Cam ',
        device_type: 'alpr',
        lat: 37,
        lng: -122,
        stream_url: 'https://x.com/s',
        source_type: 'hls',
      }
      const out = validateDeviceBody(body)
      expect(out.name).toBe('My Cam')
      expect(out.device_type).toBe('alpr')
      expect(out.lat).toBe(37)
      expect(out.lng).toBe(-122)
      expect(out.stream_url).toBe('https://x.com/s')
      expect(out.source_type).toBe('hls')
      expect(out.vendor).toBe(null)
      expect(out.config).toBe(null)
    })

    it('defaults device_type to feed when invalid', () => {
      const out = validateDeviceBody({ lat: 0, lng: 0, device_type: 'invalid' })
      expect(out.device_type).toBe('feed')
    })

    it('defaults device_type to feed when not a string', () => {
      const out = validateDeviceBody({ lat: 0, lng: 0, device_type: 99 })
      expect(out.device_type).toBe('feed')
    })

    it('throws when lat/lng missing or non-finite', () => {
      expect(() => validateDeviceBody({})).toThrow()
      expect(() => validateDeviceBody({ lat: 0, lng: Number.NaN })).toThrow()
    })

    it('accepts vendor string and config object', () => {
      const out = validateDeviceBody({
        name: 'Cam',
        lat: 0,
        lng: 0,
        vendor: '  Acme Corp  ',
        config: { resolution: '1080p' },
      })
      expect(out.vendor).toBe('Acme Corp')
      expect(out.config).toBe('{"resolution":"1080p"}')
    })
  })

  describe('constants', () => {
    it('DEVICE_TYPES includes expected types', () => {
      expect(DEVICE_TYPES).toContain('alpr')
      expect(DEVICE_TYPES).toContain('feed')
      expect(DEVICE_TYPES).toContain('traffic')
      expect(DEVICE_TYPES).toContain('ip')
      expect(DEVICE_TYPES).toContain('drone')
    })

    it('SOURCE_TYPES includes mjpeg and hls', () => {
      expect(SOURCE_TYPES).toEqual(['mjpeg', 'hls'])
    })
  })
})
