import { describe, expect, it, vi } from 'vitest'

import type { ColorAdjustOptions } from '@/utils/colorUtil'
import {
  adjustColor,
  hexToHsva,
  hexToInt,
  hexToRgb,
  hsbToRgb,
  hsvaToHex,
  isTransparent,
  parseToRgb,
  rgbToHex,
  toHexFromFormat
} from '@/utils/colorUtil'

interface ColorTestCase {
  hex: string
  rgb: string
  rgba: string
  hsl: string
  hsla: string
  lightExpected: string
  transparentExpected: string
  lightTransparentExpected: string
}

type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'

vi.mock('es-toolkit/compat', () => ({
  memoize: <T extends (...args: unknown[]) => unknown>(fn: T) => fn
}))

const targetOpacity = 0.5
const targetLightness = 0.5

const assertColorVariationsMatch = (
  variations: string[],
  adjustment: ColorAdjustOptions
) => {
  for (let i = 0; i < variations.length - 1; i++) {
    expect(adjustColor(variations[i], adjustment)).toBe(
      adjustColor(variations[i + 1], adjustment)
    )
  }
}

const colors: Record<string, ColorTestCase> = {
  green: {
    hex: '#073642',
    rgb: 'rgb(7, 54, 66)',
    rgba: 'rgba(7, 54, 66, 1)',
    hsl: 'hsl(192, 80.8%, 14.3%)',
    hsla: 'hsla(192, 80.8%, 14.3%, 1)',
    lightExpected: 'hsla(192, 80.8%, 64.3%, 1)',
    transparentExpected: 'hsla(192, 80.8%, 14.3%, 0.5)',
    lightTransparentExpected: 'hsla(192, 80.8%, 64.3%, 0.5)'
  },
  blue: {
    hex: '#00008B',
    rgb: 'rgb(0,0,139)',
    rgba: 'rgba(0,0,139,1)',
    hsl: 'hsl(240,100%,27.3%)',
    hsla: 'hsl(240,100%,27.3%,1)',
    lightExpected: 'hsla(240, 100%, 77.3%, 1)',
    transparentExpected: 'hsla(240, 100%, 27.3%, 0.5)',
    lightTransparentExpected: 'hsla(240, 100%, 77.3%, 0.5)'
  }
}

const formats: ColorFormat[] = ['hex', 'rgb', 'rgba', 'hsl', 'hsla']

describe('colorUtil conversions', () => {
  describe('hexToRgb / rgbToHex', () => {
    it('converts 6-digit hex to RGB', () => {
      expect(hexToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 })
      expect(hexToRgb('#00ff00')).toEqual({ r: 0, g: 255, b: 0 })
      expect(hexToRgb('#0000ff')).toEqual({ r: 0, g: 0, b: 255 })
    })

    it('converts 3-digit hex to RGB', () => {
      expect(hexToRgb('#f00')).toEqual({ r: 255, g: 0, b: 0 })
      expect(hexToRgb('#0f0')).toEqual({ r: 0, g: 255, b: 0 })
      expect(hexToRgb('#00f')).toEqual({ r: 0, g: 0, b: 255 })
    })

    it('converts RGB to lowercase #hex and clamps values', () => {
      expect(rgbToHex({ r: 255, g: 0, b: 0 })).toBe('#ff0000')
      expect(rgbToHex({ r: 0, g: 255, b: 0 })).toBe('#00ff00')
      expect(rgbToHex({ r: 0, g: 0, b: 255 })).toBe('#0000ff')
      // out-of-range should clamp
      expect(rgbToHex({ r: -10, g: 300, b: 16 })).toBe('#00ff10')
    })

    it('round-trips #hex -> rgb -> #hex', () => {
      const hex = '#123abc'
      expect(rgbToHex(hexToRgb(hex))).toBe('#123abc')
    })
  })

  describe('hexToInt', () => {
    it('converts 6-digit hex to packed integer', () => {
      expect(hexToInt('#ff0000')).toBe(0xff0000)
      expect(hexToInt('#00ff00')).toBe(0x00ff00)
      expect(hexToInt('#45edf5')).toBe(0x45edf5)
      expect(hexToInt('#000000')).toBe(0)
    })

    it('converts 3-digit hex to packed integer', () => {
      expect(hexToInt('#fff')).toBe(0xffffff)
      expect(hexToInt('#f00')).toBe(0xff0000)
    })
  })

  describe('parseToRgb', () => {
    it('parses #hex', () => {
      expect(parseToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 })
    })

    it('parses rgb()/rgba()', () => {
      expect(parseToRgb('rgb(255, 0, 0)')).toEqual({ r: 255, g: 0, b: 0 })
      expect(parseToRgb('rgba(255,0,0,0.5)')).toEqual({ r: 255, g: 0, b: 0 })
    })

    it('parses hsl()/hsla()', () => {
      expect(parseToRgb('hsl(0, 100%, 50%)')).toEqual({ r: 255, g: 0, b: 0 })
      const green = parseToRgb('hsla(120, 100%, 50%, 0.7)')
      expect(green.r).toBe(0)
      expect(green.g).toBe(255)
      expect(green.b).toBe(0)
    })
  })

  describe('hsbToRgb', () => {
    it('converts HSB to primary RGB colors', () => {
      expect(hsbToRgb({ h: 0, s: 100, b: 100 })).toEqual({ r: 255, g: 0, b: 0 })
      expect(hsbToRgb({ h: 120, s: 100, b: 100 })).toEqual({
        r: 0,
        g: 255,
        b: 0
      })
      expect(hsbToRgb({ h: 240, s: 100, b: 100 })).toEqual({
        r: 0,
        g: 0,
        b: 255
      })
    })

    it('handles non-100 brightness and clamps/normalizes input', () => {
      const rgb = hsbToRgb({ h: 360, s: 150, b: 50 })
      expect(rgbToHex(rgb)).toBe('#7f0000')
    })
  })

  describe('hexToHsva / hsvaToHex', () => {
    it('round-trips hex -> hsva -> hex for primary colors', () => {
      expect(hsvaToHex(hexToHsva('#ff0000'))).toBe('#ff0000')
      expect(hsvaToHex(hexToHsva('#00ff00'))).toBe('#00ff00')
      expect(hsvaToHex(hexToHsva('#0000ff'))).toBe('#0000ff')
    })

    it('handles black (v=0)', () => {
      const hsva = hexToHsva('#000000')
      expect(hsva.v).toBe(0)
      expect(hsvaToHex(hsva)).toBe('#000000')
    })

    it('handles white (s=0, v=100)', () => {
      const hsva = hexToHsva('#ffffff')
      expect(hsva.s).toBe(0)
      expect(hsva.v).toBe(100)
      expect(hsvaToHex(hsva)).toBe('#ffffff')
    })

    it('handles pure hues', () => {
      const red = hexToHsva('#ff0000')
      expect(red.h).toBeCloseTo(0)
      expect(red.s).toBeCloseTo(100)
      expect(red.v).toBeCloseTo(100)

      const green = hexToHsva('#00ff00')
      expect(green.h).toBeCloseTo(120)

      const blue = hexToHsva('#0000ff')
      expect(blue.h).toBeCloseTo(240)
    })

    it('preserves alpha=100 (no alpha suffix in hex)', () => {
      const hsva = hexToHsva('#ff0000')
      expect(hsva.a).toBe(100)
      expect(hsvaToHex(hsva)).toBe('#ff0000')
    })

    it('preserves alpha=0', () => {
      const hsva = hexToHsva('#ff000000')
      expect(hsva.a).toBe(0)
      expect(hsvaToHex(hsva)).toBe('#ff000000')
    })

    it('round-trips hex with alpha', () => {
      const hex = '#ff000080'
      const hsva = hexToHsva(hex)
      expect(hsva.a).toBe(50)
      expect(hsvaToHex(hsva)).toBe(hex)
    })

    it('handles 5-char hex with alpha', () => {
      const hsva = hexToHsva('#f008')
      expect(hsva.a).toBe(53)
      expect(hsvaToHex(hsva)).toMatch(/^#ff0000/)
    })

    // Note: a round-trip test for non-primary palette colors (e.g. #80c0ff)
    // is intentionally NOT included here. The current conversion path drifts
    // by 1 channel (hsbToRgb floors, rgbToHex rounds), so encoding that drift
    // as a passing assertion would block fixing the underlying user-visible
    // ColorPicker bug. Track the source-side fix separately.
  })

  describe('parseToRgb edge cases', () => {
    it.for(['', 'not-a-color', '#GGGGGG', 'cmky(1,2,3,4)'])(
      'returns black for unrecognized input %s',
      (input) => {
        expect(parseToRgb(input)).toEqual({ r: 0, g: 0, b: 0 })
      }
    )

    it('parses 4-digit hex with alpha and ignores the alpha channel in RGB output', () => {
      // #f008 == #ff0000 with 53% alpha; parseToRgb returns RGB only.
      expect(parseToRgb('#f008')).toEqual({ r: 255, g: 0, b: 0 })
    })

    it('parses 8-digit hex and ignores the alpha channel in RGB output', () => {
      expect(parseToRgb('#ff000080')).toEqual({ r: 255, g: 0, b: 0 })
    })
  })

  describe('hsbToRgb normalization', () => {
    it('normalizes negative hue', () => {
      // h = -120 should map to h = 240 (blue) when s and b are both 100.
      expect(hsbToRgb({ h: -120, s: 100, b: 100 })).toEqual({
        r: 0,
        g: 0,
        b: 255
      })
    })
  })

  describe('isTransparent', () => {
    it('returns true for the literal "transparent" keyword', () => {
      expect(isTransparent('transparent')).toBe(true)
    })

    it('returns true for 5-char hex with zero alpha', () => {
      expect(isTransparent('#abc0')).toBe(true)
    })

    it('returns true for 9-char hex with zero alpha', () => {
      expect(isTransparent('#abcdef00')).toBe(true)
    })

    it('returns false for fully opaque hex colors', () => {
      expect(isTransparent('#ff0000')).toBe(false)
      expect(isTransparent('#ff0000ff')).toBe(false)
    })
  })

  describe('toHexFromFormat', () => {
    it('treats an HSV object (with v field) the same as an HSB object', () => {
      const hsbObject = { h: 120, s: 100, b: 100 }
      const hsvObject = { h: 120, s: 100, v: 100 }

      expect(toHexFromFormat(hsvObject, 'hsb')).toBe(
        toHexFromFormat(hsbObject, 'hsb')
      )
      expect(toHexFromFormat(hsvObject, 'hsb')).toBe('#00ff00')
    })

    it('returns #000000 for unparseable hsb input', () => {
      expect(toHexFromFormat({ h: 0 }, 'hsb')).toBe('#000000')
    })

    it('prefixes a bare 6-digit hex with #', () => {
      expect(toHexFromFormat('abcdef', 'hex')).toBe('#abcdef')
    })
  })
})
describe('colorUtil - adjustColor', () => {
  const runAdjustColorTests = (
    color: ColorTestCase,
    format: ColorFormat
  ): void => {
    it('converts lightness', () => {
      const result = adjustColor(color[format], { lightness: targetLightness })
      expect(result).toBe(color.lightExpected)
    })

    it('applies opacity', () => {
      const result = adjustColor(color[format], { opacity: targetOpacity })
      expect(result).toBe(color.transparentExpected)
    })

    it('applies lightness and opacity jointly', () => {
      const result = adjustColor(color[format], {
        lightness: targetLightness,
        opacity: targetOpacity
      })
      expect(result).toBe(color.lightTransparentExpected)
    })
  }

  describe.for(Object.entries(colors))('%s color', ([_colorName, color]) => {
    describe.for(formats)('%s format', (format) => {
      runAdjustColorTests(color, format as ColorFormat)
    })
  })

  it('returns the original value for invalid color formats', () => {
    const invalidColors = [
      'cmky(100, 50, 50, 0.5)',
      'rgb(300, -10, 256)',
      'xyz(255, 255, 255)',
      'hsl(100, 50, 50%)',
      'hsl(100, 50%, 50)',
      '#GGGGGG'
    ]

    invalidColors.forEach((color) => {
      const result = adjustColor(color, {
        lightness: targetLightness,
        opacity: targetOpacity
      })
      expect(result).toBe(color)
    })
  })

  it('treats 5-char hex as valid color with alpha', () => {
    const result = adjustColor('#f008', {
      lightness: targetLightness,
      opacity: targetOpacity
    })
    expect(result).not.toBe('#f008')
    expect(result).toMatch(/^hsla\(/)
  })

  it('returns the original value for null or undefined inputs', () => {
    // @ts-expect-error fixme ts strict error
    expect(adjustColor(null, { opacity: targetOpacity })).toBe(null)
    // @ts-expect-error fixme ts strict error
    expect(adjustColor(undefined, { opacity: targetOpacity })).toBe(undefined)
  })

  describe('handles input variations', () => {
    it('handles spaces in rgb input', () => {
      const variations = [
        'rgb(0, 0, 0)',
        'rgb(0,0,0)',
        'rgb(0, 0,0)',
        'rgb(0,0, 0)'
      ]
      assertColorVariationsMatch(variations, { lightness: 0.5 })
    })

    it('handles spaces in hsl input', () => {
      const variations = [
        'hsl(0, 0%, 0%)',
        'hsl(0,0%,0%)',
        'hsl(0, 0%,0%)',
        'hsl(0,0%, 0%)'
      ]
      assertColorVariationsMatch(variations, { lightness: 0.5 })
    })

    it('handles different decimal places in rgba input', () => {
      const variations = [
        'rgba(0, 0, 0, 0.5)',
        'rgba(0, 0, 0, 0.50)',
        'rgba(0, 0, 0, 0.500)'
      ]
      assertColorVariationsMatch(variations, { opacity: 0.5 })
    })

    it('handles different decimal places in hsla input', () => {
      const variations = [
        'hsla(0, 0%, 0%, 0.5)',
        'hsla(0, 0%, 0%, 0.50)',
        'hsla(0, 0%, 0%, 0.500)'
      ]
      assertColorVariationsMatch(variations, { opacity: 0.5 })
    })
  })

  describe('clamps values correctly', () => {
    it('clamps lightness to 0 and 100', () => {
      expect(adjustColor('hsl(0, 100%, 50%)', { lightness: -1 })).toBe(
        'hsla(0, 100%, 0%, 1)'
      )
      expect(adjustColor('hsl(0, 100%, 50%)', { lightness: 1.5 })).toBe(
        'hsla(0, 100%, 100%, 1)'
      )
    })

    it('clamps opacity to 0 and 1', () => {
      expect(adjustColor('rgba(0, 0, 0, 0.5)', { opacity: -0.5 })).toBe(
        'hsla(0, 0%, 0%, 0)'
      )
      expect(adjustColor('rgba(0, 0, 0, 0.5)', { opacity: 1.5 })).toBe(
        'hsla(0, 0%, 0%, 1)'
      )
    })
  })
})
