import { describe, expect, it } from '@jest/globals'

import {
  applyRangeFilters,
  formatAndSortOptions,
  getMatches,
  getOptions,
  matchDateFilter,
  matchFilter,
  matchNumberFilter,
} from './filter.utils'

describe('matchFilter', () => {
  it('should return true when no filter values are provided', () => {
    expect(matchFilter('test', undefined)).toBe(true)
    expect(matchFilter('test', [])).toBe(true)
  })

  it('should match string values', () => {
    expect(matchFilter('a', ['a', 'b'])).toBe(true)
    expect(matchFilter('a', ['b', 'c'])).toBe(false)
  })

  it('should match number values', () => {
    expect(matchFilter(1, [1, 2])).toBe(true)
    expect(matchFilter(1, [2, 3])).toBe(false)
  })

  it('should match empty string and null/undefined', () => {
    expect(matchFilter(null, [''])).toBe(true)
    expect(matchFilter(undefined, [''])).toBe(true)
    expect(matchFilter('', [''])).toBe(true)
  })

  it('should not match empty value when filter does not include empty string', () => {
    expect(matchFilter(null, ['a'])).toBe(false)
    expect(matchFilter(undefined, ['a'])).toBe(false)
    expect(matchFilter('', ['a'])).toBe(false)
  })

  it('should convert Date objects to ISO string for comparison', () => {
    const date = new Date('2020-01-01')
    const isoString = date.toISOString()
    expect(matchFilter(date, [isoString])).toBe(true)
  })

  it('should convert values to strings for comparison', () => {
    expect(matchFilter(123, ['123'])).toBe(true)
    expect(matchFilter(true, ['true'])).toBe(true)
  })
})

describe('matchNumberFilter', () => {
  it('should return true when no filter is provided', () => {
    expect(matchNumberFilter(50, undefined)).toBe(true)
  })

  it('should apply range filter with min and max', () => {
    const filter = { min: 10, max: 100 }
    expect(matchNumberFilter(10, filter)).toBe(true)
    expect(matchNumberFilter(50, filter)).toBe(true)
    expect(matchNumberFilter(5, filter)).toBe(false)
    expect(matchNumberFilter(100, filter)).toBe(true)
    expect(matchNumberFilter(150, filter)).toBe(false)
  })

  it('should apply only min filter', () => {
    const filter = { min: 10 }
    expect(matchNumberFilter(50, filter)).toBe(true)
    expect(matchNumberFilter(10, filter)).toBe(true)
    expect(matchNumberFilter(5, filter)).toBe(false)
    expect(matchNumberFilter(150, filter)).toBe(true)
  })

  it('should apply only max filter', () => {
    const filter = { max: 100 }
    expect(matchNumberFilter(50, filter)).toBe(true)
    expect(matchNumberFilter(100, filter)).toBe(true)
    expect(matchNumberFilter(5, filter)).toBe(true)
    expect(matchNumberFilter(150, filter)).toBe(false)
  })

  it('should handle null values with range filters', () => {
    expect(matchNumberFilter(null, { min: 0 })).toBe(false)
    expect(matchNumberFilter(null, { max: 100 })).toBe(false)
    expect(matchNumberFilter(null, {})).toBe(true)
  })

  it('should match values in the values array', () => {
    const filter = { values: [42, 100] }
    expect(matchNumberFilter(42, filter)).toBe(true)
    expect(matchNumberFilter(100, filter)).toBe(true)
    expect(matchNumberFilter(50, filter)).toBe(false)
  })

  it('should combine range and values filters', () => {
    const filter = { min: 10, max: 100, values: [50, 150] }
    expect(matchNumberFilter(50, filter)).toBe(true)
    expect(matchNumberFilter(150, filter)).toBe(false)
    expect(matchNumberFilter(25, filter)).toBe(false)
  })
})

describe('matchDateFilter', () => {
  it('should return true when no filter is provided', () => {
    const date = new Date('2024-01-15')
    expect(matchDateFilter(date, undefined)).toBe(true)
  })

  it('should apply range filter with start and end dates', () => {
    const filter = {
      start: new Date('2024-01-01').toISOString(),
      end: new Date('2024-12-31').toISOString(),
    }
    expect(matchDateFilter(new Date('2024-06-15'), filter)).toBe(true)
    expect(matchDateFilter(new Date('2023-01-15'), filter)).toBe(false)
    expect(matchDateFilter(new Date('2025-12-15'), filter)).toBe(false)
  })

  it('should handle null dates with range filters', () => {
    expect(matchDateFilter(null, { start: '2024-01-01' })).toBe(false)
    expect(matchDateFilter(null, { end: '2024-12-31' })).toBe(false)
    expect(matchDateFilter(null, {})).toBe(true)
  })

  it('should match values in the values array', () => {
    const date1 = new Date('2024-01-15')
    const date2 = new Date('2024-06-15')
    const date1Iso = date1.toISOString()
    const date2Iso = date2.toISOString()

    const filter = { values: [date1Iso, date2Iso] }
    expect(matchDateFilter(date1, filter)).toBe(true)
    expect(matchDateFilter(date2, filter)).toBe(true)
    expect(matchDateFilter(new Date('2024-03-15'), filter)).toBe(false)
  })
})

describe('getMatches', () => {
  it('should collect string values from items', () => {
    const items = [{ name: 'apple' }, { name: 'banana' }, { name: 'apple' }]
    const matches = getMatches(items, (item) => item.name)
    expect(matches).toEqual(new Set(['apple', 'banana']))
  })

  it('should collect number values from items', () => {
    const items = [{ age: 25 }, { age: 30 }, { age: 25 }]
    const matches = getMatches(items, (item) => item.age)
    expect(matches).toEqual(new Set(['25', '30']))
  })

  it('should handle empty strings, null, and undefined as empty value', () => {
    const items = [{ status: 'active' }, { status: null }, { status: undefined }]
    const matches = getMatches(items, (item) => item.status)
    expect(matches).toEqual(new Set(['active', '']))
  })

  it('should convert Date objects to ISO strings', () => {
    const date1 = new Date('2024-01-15')
    const date2 = new Date('2024-06-15')
    const items = [{ date: date1 }, { date: date2 }, { date: date1 }]
    const matches = getMatches(items, (item) => item.date)
    expect(matches).toEqual(new Set([date1.toISOString(), date2.toISOString()]))
  })

  it('should handle empty array', () => {
    const matches = getMatches([], (item: any) => item.field)
    expect(matches).toEqual(new Set())
  })
})

describe('getOptions', () => {
  it('should generate enum options', () => {
    const items = [{ status: 'active' }, { status: 'inactive' }]
    const config = {
      kind: 'enum' as const,
      valueType: 'string' as const,
      filterKey: 'status' as any,
      enumName: 'Status',
      enumValues: ['active', 'inactive', 'pending'],
    }
    const options = getOptions(items, (item) => item.status, config)
    expect(options).toEqual([
      { value: 'active', label: 'active' },
      { value: 'inactive', label: 'inactive' },
      { value: 'pending', label: 'pending' },
    ])
  })

  it('should generate boolean options', () => {
    const items = [{ isActive: true }, { isActive: false }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'boolean' as const,
      filterKey: 'isActive' as any,
    }
    const options = getOptions(items, (item) => item.isActive, config)
    expect(options).toEqual([
      { value: 'true', label: 'Yes' },
      { value: 'false', label: 'No' },
    ])
  })

  it('should generate string/scalar options', () => {
    const items = [{ name: 'apple' }, { name: 'banana' }, { name: 'apple' }, { name: null }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'string' as const,
      filterKey: 'name' as any,
    }
    const options = getOptions(items, (item) => item.name, config)
    expect(options.length).toBe(3)
    expect(options).toContainEqual({ value: 'apple', label: 'apple' })
    expect(options).toContainEqual({ value: 'banana', label: 'banana' })
    expect(options).toContainEqual({ value: '', label: 'Empty' })
  })

  it('should generate relation options from referencedItems', () => {
    const items = [{ userId: '1' }, { userId: '2' }, { userId: '1' }]
    const referencedItems = new Map([
      ['1', { id: '1', name: 'User One' }],
      ['2', { id: '2', name: 'User Two' }],
    ])
    const config = {
      kind: 'relation' as const,
      valueType: 'string' as const,
      filterKey: 'userId' as any,
      referencedModelViewServiceVariableName: 'users',
      referencedModelLabelField: 'name',
      referencedModelIdField: 'id',
    }
    const options = getOptions(items, (item) => item.userId, config, referencedItems)
    expect(options).toEqual([
      { value: '1', label: 'User One' },
      { value: '2', label: 'User Two' },
    ])
  })

  it('should include empty option for relation with null values', () => {
    const items = [{ userId: '1' }, { userId: null }]
    const referencedItems = new Map([['1', { id: '1', name: 'User One' }]])
    const config = {
      kind: 'relation' as const,
      valueType: 'string' as const,
      filterKey: 'userId' as any,
      referencedModelViewServiceVariableName: 'users',
      referencedModelLabelField: 'name',
      referencedModelIdField: 'id',
    }
    const options = getOptions(items, (item) => item.userId, config, referencedItems)
    expect(options).toContainEqual({ value: '', label: 'Empty' })
  })
})

describe('applyRangeFilters', () => {
  it('should apply number range filters', () => {
    const items = [{ age: 20 }, { age: 50 }, { age: 80 }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'number' as const,
      filterKey: 'age' as any,
    }
    const filters = { age: { min: 30, max: 60 } }
    const result = applyRangeFilters(items, (item) => item.age, config, filters)
    expect(result).toEqual([{ age: 50 }])
  })

  it('should apply date range filters', () => {
    const date1 = new Date('2024-01-15')
    const date2 = new Date('2024-06-15')
    const date3 = new Date('2024-12-15')
    const items = [{ date: date1 }, { date: date2 }, { date: date3 }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'date' as const,
      filterKey: 'date' as any,
    }
    const filters = {
      date: {
        start: new Date('2024-03-01').toISOString(),
        end: new Date('2024-09-01').toISOString(),
      },
    }
    const result = applyRangeFilters(items, (item) => item.date, config, filters)
    expect(result).toEqual([{ date: date2 }])
  })

  it('should return all items when no range filter is provided', () => {
    const items = [{ age: 20 }, { age: 50 }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'number' as const,
      filterKey: 'age' as any,
    }
    const filters = { age: undefined }
    const result = applyRangeFilters(items, (item) => item.age, config, filters)
    expect(result).toEqual(items)
  })

  it('should not apply range filters for non-number/date fields', () => {
    const items = [{ name: 'apple' }, { name: 'banana' }]
    const config = {
      kind: 'scalar' as const,
      valueType: 'string' as const,
      filterKey: 'name' as any,
    }
    const filters = { name: { min: 0, max: 10 } as any }
    const result = applyRangeFilters(items, (item) => item.name, config, filters)
    expect(result).toEqual(items)
  })
})

describe('formatAndSortOptions', () => {
  it('should add hasMatches flag to options', () => {
    const options = [
      { value: 'apple', label: 'Apple' },
      { value: 'banana', label: 'Banana' },
    ]
    const matches = new Set(['apple'])
    const result = formatAndSortOptions(options, matches)
    expect(result).toContainEqual({ value: 'apple', label: 'Apple', hasMatches: true })
    expect(result).toContainEqual({ value: 'banana', label: 'Banana', hasMatches: false })
  })

  it('should sort options with matches first', () => {
    const options = [
      { value: 'a', label: 'A' },
      { value: 'b', label: 'B' },
      { value: 'c', label: 'C' },
    ]
    const matches = new Set(['b', 'c'])
    const result = formatAndSortOptions(options, matches)
    expect(result[0]?.hasMatches).toBe(true)
    expect(result[1]?.hasMatches).toBe(true)
    expect(result[2]?.hasMatches).toBe(false)
  })

  it('should place empty option first among matched items', () => {
    const options = [
      { value: 'apple', label: 'Apple' },
      { value: '', label: 'Empty' },
    ]
    const matches = new Set(['', 'apple'])
    const result = formatAndSortOptions(options, matches)
    expect(result[0]?.value).toBe('')
  })

  it('should place empty option first among non-matched items', () => {
    const options = [
      { value: 'apple', label: 'Apple' },
      { value: '', label: 'Empty' },
    ]
    const matches = new Set(['apple'])
    const result = formatAndSortOptions(options, matches)
    expect(result[1]?.value).toBe('')
  })

  it('should sort options alphabetically by label when match status is equal', () => {
    const options = [
      { value: 'c', label: 'Charlie' },
      { value: 'a', label: 'Apple' },
      { value: 'b', label: 'Banana' },
    ]
    const matches = new Set<string>([])
    const result = formatAndSortOptions(options, matches)
    expect(result[0]?.label).toBe('Apple')
    expect(result[1]?.label).toBe('Banana')
    expect(result[2]?.label).toBe('Charlie')
  })

  it('should handle empty options array', () => {
    const options: { value: string; label: string }[] = []
    const matches = new Set<string>()
    const result = formatAndSortOptions(options, matches)
    expect(result).toEqual([])
  })

  it('should handle empty matches set', () => {
    const options = [
      { value: 'apple', label: 'Apple' },
      { value: 'banana', label: 'Banana' },
    ]
    const matches = new Set<string>()
    const result = formatAndSortOptions(options, matches)
    expect(result.every((opt) => opt.hasMatches === false)).toBe(true)
  })
})
