import { describe, it, expect, beforeEach } from 'vitest';
import '../../core/element.js';
import './slider.js';

const tick = () => new Promise((r) => queueMicrotask(r));

function mount(html) {
  const wrap = document.createElement('div');
  wrap.innerHTML = html;
  document.body.appendChild(wrap);
  return wrap.firstElementChild;
}

describe('slider-ui', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  // §330 (v0.5.16) — slider iOS-style pill redesign. Thumb position no
  // longer set via thumb.style.left; instead the host gets a CSS custom
  // property `--slider-pct` (fraction 0.0–1.0) and CSS calc() composes
  // the full geometry: `fill_width(p) = t + p·(W−t)`, where t = thumb
  // width and W = track width. Test asserts the CSS variable carries
  // the expected fraction (50% → 0.5).
  const pct = (s) => s.style.getPropertyValue('--slider-pct').trim();

  it('renders host --slider-pct for initial value', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();
    expect(pct(s)).toBe('0.5');
  });

  // Property reactivity contract — locks in the behavior that consumer
  // feedback (FEEDBACK-adia-packages.md §5, 2026-05-12) incorrectly
  // claimed was broken. UIElement's signal-backed property setters
  // trigger the host's render effect on every property change; slider-ui's
  // render() reads this.value and writes --slider-pct on the host.
  it('updates --slider-pct when .value is set programmatically', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();
    expect(pct(s)).toBe('0.5');

    s.value = 75;
    await tick();
    expect(pct(s)).toBe('0.75');

    s.value = 10;
    await tick();
    expect(pct(s)).toBe('0.1');
  });

  it('updates --slider-pct when attribute is set imperatively', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();

    s.setAttribute('value', '25');
    await tick();
    expect(pct(s)).toBe('0.25');
  });

  it('updates [slot="value"] readout text when .value changes', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();
    const readout = s.querySelector('[slot="value"]');
    expect(readout.textContent).toBe('50');

    s.value = 88;
    await tick();
    expect(readout.textContent).toBe('88');
  });

  it('emits CustomEvent("change") with detail.value on keyboard step', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
    await tick();

    let captured = null;
    s.addEventListener('change', (e) => { captured = e; });

    s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
    await tick();

    expect(captured).not.toBeNull();
    expect(captured).toBeInstanceOf(CustomEvent);
    expect(captured.detail).toEqual({ value: 51 });
  });

  it('emits CustomEvent("input") with detail.value on internal value change', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
    await tick();

    let captured = null;
    s.addEventListener('input', (e) => { captured = e; });

    s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
    await tick();

    expect(captured).not.toBeNull();
    expect(captured.detail).toEqual({ value: 51 });
  });

  it('reflects value to [aria-valuenow] for screen readers', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();
    expect(s.getAttribute('aria-valuenow')).toBe('50');

    s.value = 80;
    await tick();
    expect(s.getAttribute('aria-valuenow')).toBe('80');
  });

  // §FB-45 (color-app, 2026-05-22) — template engine snapshot trap.
  // connected() stamped label/suffix once; render() didn't re-sync them.
  // Template engine writes {{p:N}} first, then resolves to the real value —
  // simulated here by setAttribute('label', placeholder) then setting the
  // real value. Both spans must reflect the final value, not the placeholder.
  it('re-syncs [slot="label"] when label attribute changes after connection', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
    await tick();
    // No label initially — span absent
    expect(s.querySelector('[slot="label"]')).toBeNull();

    // Simulate template engine writing placeholder then real value
    s.setAttribute('label', '{{p:7}}');
    await tick();
    s.setAttribute('label', 'Volume');
    await tick();

    const labelEl = s.querySelector('[slot="label"]');
    expect(labelEl).not.toBeNull();
    expect(labelEl.textContent).toBe('Volume');
    expect(s.getAttribute('aria-label')).toBe('Volume');
  });

  it('re-syncs [slot="suffix"] when suffix attribute changes after connection', async () => {
    const s = mount('<slider-ui value="50" min="0" max="100" label="Volume" suffix="%"></slider-ui>');
    await tick();
    const suffixEl = s.querySelector('[slot="suffix"]');
    expect(suffixEl).not.toBeNull();
    expect(suffixEl.textContent).toBe('%');

    // Simulate placeholder → resolved suffix
    s.setAttribute('suffix', '{{p:8}}');
    await tick();
    s.setAttribute('suffix', 'rem');
    await tick();

    expect(s.querySelector('[slot="suffix"]').textContent).toBe('rem');
  });

  // ─── Dual-thumb mode ────────────────────────────────────────────────
  // v1.9.x: dual-thumb range slider lives on slider-ui[dual] + [lower-value]/[upper-value].
  // Fill spans BETWEEN the two thumb centers; thumbs are positioned via
  // --slider-pct-lower / --slider-pct-upper CSS variables.
  const pctLower = (s) => s.style.getPropertyValue('--slider-pct-lower').trim();
  const pctUpper = (s) => s.style.getPropertyValue('--slider-pct-upper').trim();

  it('dual mode: writes --slider-pct-lower and --slider-pct-upper from lower-value / upper-value', async () => {
    const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
    await tick();
    expect(pctLower(s)).toBe('0.2');
    expect(pctUpper(s)).toBe('0.8');
  });

  it('dual mode: stamps two thumb elements with data-thumb="lower" and data-thumb="upper"', async () => {
    const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
    await tick();
    const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
    const upperThumb = s.querySelector('[slot="thumb"][data-thumb="upper"]');
    expect(lowerThumb).not.toBeNull();
    expect(upperThumb).not.toBeNull();
  });

  it('dual mode: change event detail carries {lower, upper}', async () => {
    const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
    await tick();
    let captured = null;
    s.addEventListener('change', (e) => { captured = e; });
    // Focus the lower thumb explicitly so the keyboard handler targets it
    const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
    lowerThumb.focus();
    lowerThumb.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
    await tick();
    expect(captured).not.toBeNull();
    expect(captured.detail).toEqual({ lower: 21, upper: 80 });
  });

  it('dual mode: lower setter clamps DOWN to upper when set too high', async () => {
    const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
    await tick();
    s.lowerValue = 90; // > upperValue (80) → constraint clamps lower DOWN to upper
    await tick();
    expect(s.lowerValue).toBe(80);
    expect(s.upperValue).toBe(80); // upper unchanged
  });

  it('dual mode: upper setter clamps UP to lower when set too low', async () => {
    const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
    await tick();
    s.upperValue = 10; // < lowerValue (20) → constraint clamps upper UP to lower
    await tick();
    expect(s.upperValue).toBe(20);
    expect(s.lowerValue).toBe(20); // lower unchanged
  });
});
