/**
 * date-range-picker-ui — unit tests covering the SPEC-037 contract.
 *
 * Run under happy-dom (per vitest.config.js). showPopover/hidePopover are
 * stubbed to no-ops by `packages/web-components/test-setup.js`, so popover
 * state is tracked via the `open` reflected attribute and the `<popover>`
 * div's presence, not via `:popover-open` selector matching.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import '../../core/element.js';
import '../button/button.js';
import '../popover/popover.js';
import '../calendar-picker/calendar-picker.js';
import '../divider/divider.js';
import './date-range-picker.js';

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

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

describe('date-range-picker-ui — structure + defaults', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('registers the custom element', () => {
    expect(customElements.get('date-range-picker-ui')).toBeTruthy();
  });

  it('stamps a default <button-ui slot="trigger"> when no slot="trigger" is authored', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger).not.toBeNull();
    expect(trigger.localName).toBe('button-ui');
  });

  it('stamps a popover container with role=dialog', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    expect(popover).not.toBeNull();
    expect(popover.getAttribute('role')).toBe('dialog');
  });

  it('stamps TWO <calendar-grid-ui> instances inside the popover', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const calArea = el.querySelector(':scope > [slot="popover"] > [data-calendar-area]');
    expect(calArea).not.toBeNull();
    const cals = calArea.querySelectorAll(':scope > calendar-grid-ui');
    expect(cals.length).toBe(2);
    expect(cals[0].hasAttribute('data-cal-from')).toBe(true);
    expect(cals[1].hasAttribute('data-cal-to')).toBe(true);
  });

  it('stamps a preset rail with the default 5 presets', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
    expect(rail).not.toBeNull();
    const buttons = rail.querySelectorAll('button-ui[data-preset-label]');
    expect(buttons.length).toBe(5);
    expect(buttons[0].dataset.presetLabel).toBe('Today');
  });

  it('host carries role=combobox + aria-haspopup=dialog', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    expect(el.getAttribute('role')).toBe('combobox');
    expect(el.getAttribute('aria-haspopup')).toBe('dialog');
    expect(el.getAttribute('aria-expanded')).toBe('false');
  });
});

describe('date-range-picker-ui — props + reflection', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('reflects [min] and [max] onto both calendar panes', async () => {
    const el = mount('<date-range-picker-ui min="2026-01-01" max="2026-12-31"></date-range-picker-ui>');
    await tick();
    const cals = el.querySelectorAll('calendar-grid-ui');
    expect(cals[0].getAttribute('min')).toBe('2026-01-01');
    expect(cals[0].getAttribute('max')).toBe('2026-12-31');
    expect(cals[1].getAttribute('min')).toBe('2026-01-01');
    expect(cals[1].getAttribute('max')).toBe('2026-12-31');
  });

  it('parses JSON `value` into rangeValue', async () => {
    const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
    await tick();
    expect(el.rangeValue).toEqual({ from: '2026-01-01', to: '2026-01-07' });
  });

  it('renders formatted-range text in the trigger when value is set', async () => {
    const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toContain('Jan 1, 2026');
    expect(trigger.getAttribute('text')).toContain('Jan 7, 2026');
  });

  it('uses [placeholder] in the trigger when value is empty', async () => {
    const el = mount('<date-range-picker-ui placeholder="Choose range"></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toBe('Choose range');
  });

  it('reflects [comparison] as an attribute', async () => {
    const el = mount('<date-range-picker-ui comparison></date-range-picker-ui>');
    await tick();
    expect(el.hasAttribute('comparison')).toBe(true);
    expect(el.comparison).toBe(true);
  });

  it('[no-presets] removes the preset rail', async () => {
    const el = mount('<date-range-picker-ui no-presets></date-range-picker-ui>');
    await tick();
    expect(el.querySelector(':scope > [slot="popover"] > [data-preset-rail]')).toBeNull();
  });

  it('custom `presets` property overrides the default list', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    el.presets = [
      { label: 'Q1', range: { from: '2026-01-01', to: '2026-03-31' } },
      { label: 'Q2', range: { from: '2026-04-01', to: '2026-06-30' } },
    ];
    await tick();
    const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
    const buttons = rail.querySelectorAll('button-ui[data-preset-label]');
    expect(buttons.length).toBe(2);
    expect(buttons[0].dataset.presetLabel).toBe('Q1');
    expect(buttons[1].dataset.presetLabel).toBe('Q2');
  });
});

describe('date-range-picker-ui — open/close behavior', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('opens on trigger click + emits `open` event', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let openDetail = null;
    el.addEventListener('open', (e) => { openDetail = e.detail; });
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    trigger.click();
    await tick();
    expect(el.open).toBe(true);
    expect(el.hasAttribute('open')).toBe(true);
    expect(el.getAttribute('aria-expanded')).toBe('true');
    expect(openDetail?.trigger).toBe('click');
  });

  it('closes on second trigger click + emits `close`', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    let closeDetail = null;
    el.addEventListener('close', (e) => { closeDetail = e.detail; });
    trigger.click();
    await tick();
    trigger.click();
    await tick();
    expect(el.open).toBe(false);
    expect(closeDetail).not.toBeNull();
  });

  it('Escape closes the popover + emits close with reason "escape"', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    el.open = true;
    await tick();
    let reason = null;
    el.addEventListener('close', (e) => { reason = e.detail?.reason; });
    const popover = el.querySelector(':scope > [slot="popover"]');
    const ev = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
    popover.dispatchEvent(ev);
    await tick();
    expect(el.open).toBe(false);
    expect(reason).toBe('escape');
  });

  it('Enter on the trigger opens the popover with trigger=keyboard', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    let openDetail = null;
    el.addEventListener('open', (e) => { openDetail = e.detail; });
    trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
    await tick();
    expect(el.open).toBe(true);
    expect(openDetail?.trigger).toBe('keyboard');
  });

  it('[disabled] blocks the trigger from opening', async () => {
    const el = mount('<date-range-picker-ui disabled></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    trigger.click();
    await tick();
    expect(el.open).toBe(false);
  });

  it('[readonly] blocks the popover from opening', async () => {
    const el = mount('<date-range-picker-ui readonly></date-range-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    trigger.click();
    await tick();
    expect(el.open).toBe(false);
  });
});

describe('date-range-picker-ui — selection + commit', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('clicking a preset commits value + closes popover + fires `change` with presetLabel', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    el.open = true;
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
    const todayBtn = rail.querySelector('button-ui[data-preset-label="Today"]');
    todayBtn.click();
    await tick();
    expect(detail).not.toBeNull();
    expect(detail.presetLabel).toBe('Today');
    expect(detail.value.from).toBe(detail.value.to);
    expect(el.open).toBe(false);
    expect(el.rangeValue).not.toBeNull();
  });

  it('applyPreset(label) commits the matching preset', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const result = el.applyPreset('Last 7 days');
    expect(result).toBe(true);
    await tick();
    expect(detail).not.toBeNull();
    expect(detail.presetLabel).toBe('Last 7 days');
    expect(detail.value.from < detail.value.to).toBe(true);
  });

  it('applyPreset(unknown) returns false and does not commit', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let fired = false;
    el.addEventListener('change', () => { fired = true; });
    expect(el.applyPreset('NonExistent')).toBe(false);
    expect(fired).toBe(false);
  });

  it('emits `input` (partial state) when only `from` is selected', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let inputDetail = null;
    let changeFired = false;
    el.addEventListener('input', (e) => { inputDetail = e.detail; });
    el.addEventListener('change', () => { changeFired = true; });
    const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
    calFrom.value = '2026-03-15';
    calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
    await tick();
    expect(inputDetail).not.toBeNull();
    expect(inputDetail.value.from).toBe('2026-03-15');
    expect(inputDetail.value.to).toBeNull();
    expect(changeFired).toBe(false);
  });

  it('commits `change` when both panes have selections in order', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
    const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
    calFrom.value = '2026-03-01';
    calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
    await tick();
    calTo.value = '2026-03-15';
    calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
    await tick();
    expect(detail).not.toBeNull();
    expect(detail.value).toEqual({ from: '2026-03-01', to: '2026-03-15' });
  });

  it('reversed range fires `invalid`, not `change`', async () => {
    const el = mount('<date-range-picker-ui></date-range-picker-ui>');
    await tick();
    let invalidDetail = null;
    let changeFired = false;
    el.addEventListener('invalid', (e) => { invalidDetail = e.detail; });
    el.addEventListener('change', () => { changeFired = true; });
    const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
    const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
    // Click `to` first (with a date), then `from` AFTER it (reversed).
    calTo.value = '2026-03-01';
    calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
    await tick();
    calFrom.value = '2026-03-15';
    calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
    await tick();
    expect(invalidDetail).not.toBeNull();
    expect(invalidDetail.reason).toBe('reversed');
    expect(changeFired).toBe(false);
  });

  it('clear() resets the value', async () => {
    const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
    await tick();
    el.clear();
    await tick();
    expect(el.value).toBe('');
    expect(el.rangeValue).toBeNull();
  });
});

describe('date-range-picker-ui — comparison mode', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('adds a comparison preset cluster when [comparison] is set', async () => {
    const el = mount('<date-range-picker-ui comparison></date-range-picker-ui>');
    await tick();
    const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
    const cmpButtons = rail.querySelectorAll('button-ui[data-comparison-preset-label]');
    expect(cmpButtons.length).toBe(3);
    expect(cmpButtons[0].dataset.comparisonPresetLabel).toBe('Previous period');
  });

  it('parses compare-value into rangeCompareValue', async () => {
    const el = mount('<date-range-picker-ui comparison compare-value=\'{"from":"2026-04-01","to":"2026-04-30"}\'></date-range-picker-ui>');
    await tick();
    expect(el.rangeCompareValue).toEqual({ from: '2026-04-01', to: '2026-04-30' });
  });

  it('change detail carries compareValue when comparison is set', async () => {
    const el = mount('<date-range-picker-ui comparison compare-value=\'{"from":"2026-04-01","to":"2026-04-30"}\'></date-range-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
    const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
    calFrom.value = '2026-05-01';
    calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-05-01' } }));
    await tick();
    calTo.value = '2026-05-31';
    calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-05-31' } }));
    await tick();
    expect(detail).not.toBeNull();
    expect(detail.value).toEqual({ from: '2026-05-01', to: '2026-05-31' });
    expect(detail.compareValue).toEqual({ from: '2026-04-01', to: '2026-04-30' });
  });
});

describe('date-range-picker-ui — form participation', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('is form-associated', () => {
    expect(customElements.get('date-range-picker-ui').formAssociated).toBe(true);
  });

  it('exposes ElementInternals via the host', async () => {
    const el = mount('<date-range-picker-ui name="period"></date-range-picker-ui>');
    await tick();
    expect(el.internals).toBeDefined();
    expect(typeof el.internals.setFormValue).toBe('function');
  });

  it('runs constraint validation on a reversed range (post-commit attempt is rejected)', async () => {
    const el = mount('<date-range-picker-ui name="period" value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
    await tick();
    // Apply a reversed range programmatically via direct value write,
    // then sync — happy-dom's setValidity is a no-op, so we just confirm
    // syncValue doesn't throw.
    expect(() => el.syncValue('{"from":"2026-12-31","to":"2026-01-01"}')).not.toThrow();
  });
});
