import { describe, it, expect } from 'vitest'
import { srgbToOklch, oklchToHex, generateRamp } from './oklch'
import { parseHex } from './color'

// Independent reference OKLab L (canonical Ottosson matrices) — NOT the impl converter.
// Measures generated-step lightness without trusting the code under test (anti-circularity).
function refOklabL(hex: string): number {
  const h = hex.replace('#', '')
  const chan = (i: number): number => {
    let c = parseInt(h.slice(i, i + 2), 16) / 255
    c = c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
    return c
  }
  const r = chan(0), g = chan(2), b = chan(4)
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
  const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
  return 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s
}

// Independent WCAG contrast for the step-9 best-polarity assertion.
function relLum(hex: string): number {
  const rgb = parseHex(hex)!
  const f = (v: number): number => {
    const x = v / 255
    return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
  }
  return 0.2126 * f(rgb[0]!) + 0.7152 * f(rgb[1]!) + 0.0722 * f(rgb[2]!)
}
function ratio(a: string, b: string): number {
  const la = relLum(a), lb = relLum(b)
  return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
}

describe('srgbToOklch — canonical OKLab (discriminates vs CIELAB)', () => {
  // Baked independent reference L (Ottosson). CIELAB L*/100 diverges sharply,
  // e.g. #0000ff OKLab 0.452 vs CIELAB 0.323 → a CIELAB impl fails toBeCloseTo(_,2).
  const cases: ReadonlyArray<readonly [string, number]> = [
    ['#0000ff', 0.452], ['#ff0000', 0.628], ['#00ff00', 0.866],
    ['#3b82f6', 0.623], ['#ffffff', 1.0], ['#808080', 0.6], ['#000000', 0.0],
  ]
  for (const [hex, L] of cases) {
    it(`${hex} L ≈ ${L}`, () => {
      const r = srgbToOklch(hex)
      expect(r).not.toBeNull()
      expect(r!.L).toBeCloseTo(L, 2)
    })
  }
  it('achromatic → C ≈ 0', () => {
    expect(srgbToOklch('#ffffff')!.C).toBeCloseTo(0, 2)
    expect(srgbToOklch('#808080')!.C).toBeCloseTo(0, 2)
  })
  it('fail-closed on non-hex', () => {
    expect(srgbToOklch('not-a-hex')).toBeNull()
    expect(srgbToOklch('rgb(0,0,0)')).toBeNull()
  })
})

describe('oklchToHex — inverse round-trips in-gamut seeds', () => {
  for (const hex of ['#3b82f6', '#ff0000', '#1b5e20', '#8a4b00']) {
    it(`round-trips ${hex}`, () => {
      const o = srgbToOklch(hex)!
      const back = oklchToHex(o.L, o.C, o.H)
      const a = parseHex(hex)!, b = parseHex(back)!
      for (let i = 0; i < 3; i++) expect(Math.abs(a[i]! - b[i]!)).toBeLessThanOrEqual(2)
    })
  }
})

describe('generateRamp — ModePair drop-in with DEFAULT_RAMPS', () => {
  const roles = ['neutral', 'accent', 'intent'] as const
  const seedFor = (role: (typeof roles)[number]): string => (role === 'neutral' ? '#64748b' : '#3b82f6')

  it('fail-closed on bad seed', () => {
    expect(generateRamp('rgb(0,0,0)', 'accent')).toBeNull()
    expect(generateRamp('nope', 'neutral')).toBeNull()
  })

  for (const role of roles) {
    const seed = seedFor(role)

    it(`${role}: ModePair shape — 12 unique valid-hex steps + 12 alpha + contrast, both modes`, () => {
      const mp = generateRamp(seed, role)
      expect(mp).not.toBeNull()
      for (const mode of ['light', 'dark'] as const) {
        const ramp = mp![mode]
        expect(ramp.steps.length).toBe(12)
        expect(ramp.alpha.length).toBe(12)
        expect(new Set(ramp.steps).size).toBe(12)
        for (const s of ramp.steps) expect(parseHex(s)).not.toBeNull()
        expect(parseHex(ramp.contrast)).not.toBeNull()
      }
    })

    it(`${role}: OKLab L monotonic (light desc, dark asc) — independent ref converter`, () => {
      const mp = generateRamp(seed, role)!
      const lightL = mp.light.steps.map(refOklabL)
      const darkL = mp.dark.steps.map(refOklabL)
      for (let i = 1; i < 12; i++) {
        expect(lightL[i]!).toBeLessThan(lightL[i - 1]!)
        expect(darkL[i]!).toBeGreaterThan(darkL[i - 1]!)
      }
    })

    it(`${role}: contrast = best-of-white/black on step-9, ratio ≥ 4.5`, () => {
      const mp = generateRamp(seed, role)!
      for (const mode of ['light', 'dark'] as const) {
        const fill = mp[mode].steps[8]!
        const chosen = mp[mode].contrast
        const best = Math.max(ratio('#ffffff', fill), ratio('#000000', fill))
        expect(ratio(chosen, fill)).toBeGreaterThanOrEqual(4.5)
        expect(ratio(chosen, fill)).toBeGreaterThanOrEqual(best - 0.01)
      }
    })
  }

  it('deterministic — same seed → deep-equal ModePair', () => {
    expect(generateRamp('#3b82f6', 'accent')).toEqual(generateRamp('#3b82f6', 'accent'))
  })
})
