import { fromAny } from '@total-typescript/shoehorn'
import { describe, expect, it } from 'vitest'

import type {
  ComboInputSpec,
  ComboInputSpecV2,
  FloatInputSpec,
  InputSpec,
  IntInputSpec
} from '@/schemas/nodeDefSchema'
import { mergeInputSpec } from '@/utils/nodeDefUtil'

describe('nodeDefUtil', () => {
  describe('mergeInputSpec', () => {
    // Test numeric input specs (INT and FLOAT)
    describe('numeric input specs', () => {
      it('should merge INT specs with overlapping ranges', () => {
        const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
        const spec2: IntInputSpec = ['INT', { min: 5, max: 15 }]

        const result = mergeInputSpec(spec1, spec2) as IntInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('INT')
        expect(result?.[1]?.min).toBe(5)
        expect(result?.[1]?.max).toBe(10)
      })

      it('should return null for INT specs with non-overlapping ranges', () => {
        const spec1: IntInputSpec = ['INT', { min: 0, max: 5 }]
        const spec2: IntInputSpec = ['INT', { min: 10, max: 15 }]

        const result = mergeInputSpec(spec1, spec2)

        expect(result).toBeNull()
      })

      it('should merge FLOAT specs with overlapping ranges', () => {
        const spec1: FloatInputSpec = ['FLOAT', { min: 0.5, max: 10.5 }]
        const spec2: FloatInputSpec = ['FLOAT', { min: 5.5, max: 15.5 }]

        const result = mergeInputSpec(spec1, spec2) as FloatInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('FLOAT')
        expect(result?.[1]?.min).toBe(5.5)
        expect(result?.[1]?.max).toBe(10.5)
      })

      it('should handle specs with undefined min/max values', () => {
        const spec1: FloatInputSpec = ['FLOAT', { min: 0.5 }]
        const spec2: FloatInputSpec = ['FLOAT', { max: 15.5 }]

        const result = mergeInputSpec(spec1, spec2) as FloatInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('FLOAT')
        expect(result?.[1]?.min).toBe(0.5)
        expect(result?.[1]?.max).toBe(15.5)
      })

      it('should merge step values using least common multiple', () => {
        const spec1: IntInputSpec = ['INT', { min: 0, max: 10, step: 2 }]
        const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 3 }]

        const result = mergeInputSpec(spec1, spec2) as IntInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('INT')
        expect(result?.[1]?.step).toBe(6) // LCM of 2 and 3 is 6
      })

      it('should use default step of 1 when step is not specified', () => {
        const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
        const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 4 }]

        const result = mergeInputSpec(spec1, spec2) as IntInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('INT')
        expect(result?.[1]?.step).toBe(4) // LCM of 1 and 4 is 4
      })

      it('should handle step values for FLOAT specs', () => {
        const spec1: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.5 }]
        const spec2: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.25 }]

        const result = mergeInputSpec(spec1, spec2) as FloatInputSpec | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('FLOAT')
        expect(result?.[1]?.step).toBe(0.5)
      })
    })

    // Test combo input specs
    describe('combo input specs', () => {
      it('should merge COMBO specs with overlapping options', () => {
        const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B', 'C'] }]
        const spec2: ComboInputSpecV2 = ['COMBO', { options: ['B', 'C', 'D'] }]

        const result = mergeInputSpec(spec1, spec2) as ComboInputSpecV2 | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('COMBO')
        expect(result?.[1]?.options).toEqual(['B', 'C'])
      })

      it('should return null for COMBO specs with no overlapping options', () => {
        const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]
        const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]

        const result = mergeInputSpec(spec1, spec2)

        expect(result).toBeNull()
      })

      it('should handle COMBO specs with additional properties', () => {
        const spec1: ComboInputSpecV2 = [
          'COMBO',
          {
            options: ['A', 'B', 'C'],
            default: 'A',
            tooltip: 'Select an option'
          }
        ]
        const spec2: ComboInputSpecV2 = [
          'COMBO',
          {
            options: ['B', 'C', 'D'],
            default: 'B',
            multiline: true
          }
        ]

        const result = mergeInputSpec(spec1, spec2) as ComboInputSpecV2 | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('COMBO')
        expect(result?.[1]?.options).toEqual(['B', 'C'])
        expect(result?.[1]?.default).toBe('B')
        expect(result?.[1]?.tooltip).toBe('Select an option')
        expect(result?.[1]?.multiline).toBe(true)
      })

      it('should handle v1 and v2 combo specs', () => {
        const spec1: ComboInputSpec = [['A', 'B', 'C', 'D'], {}]
        const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]

        const result = mergeInputSpec(spec1, spec2) as ComboInputSpecV2 | null

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('COMBO')
        expect(result?.[1]?.options).toEqual(['C', 'D'])
      })
    })

    // Test common input spec behavior
    describe('common input spec behavior', () => {
      it('should return null for specs with different types', () => {
        const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
        const spec2: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]

        const result = mergeInputSpec(
          spec1,
          fromAny<IntInputSpec, unknown>(spec2)
        )

        expect(result).toBeNull()
      })

      it('should ignore specified keys when comparing specs', () => {
        const spec1: InputSpec = [
          'STRING',
          {
            default: 'value1',
            tooltip: 'Tooltip 1',
            step: 1
          }
        ]
        const spec2: InputSpec = [
          'STRING',
          {
            default: 'value2',
            tooltip: 'Tooltip 2',
            step: 1
          }
        ]

        const result = mergeInputSpec(spec1, spec2)

        expect(result).not.toBeNull()
        expect(result?.[0]).toBe('STRING')
        const options = result?.[1] as
          | { default?: string; tooltip?: string; step?: number }
          | undefined
        expect(options?.default).toBe('value2')
        expect(options?.tooltip).toBe('Tooltip 2')
        expect(options?.step).toBe(1)
      })

      it('should return null if non-ignored properties differ', () => {
        const spec1: InputSpec = ['STRING', { step: 1 }]
        const spec2: InputSpec = ['STRING', { step: 2 }]

        const result = mergeInputSpec(spec1, spec2)

        expect(result).toBeNull()
      })
    })
  })
})
