/**
 * datetime-picker-ui — unit tests covering the SPEC-038 contract.
 *
 * Composition: <calendar-picker-ui> (date pane) + <time-picker-ui>
 * (time pane) inside a single <popover>. The host emits an ISO 8601
 * datetime string on commit.
 *
 * 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 '../time-picker/time-picker.js';
import '../divider/divider.js';
import './datetime-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('datetime-picker-ui — structure + defaults', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

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

  it('stamps a default <button-ui slot="trigger"> when no trigger is authored', async () => {
    const el = mount('<datetime-picker-ui></datetime-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('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    expect(popover).not.toBeNull();
    expect(popover.getAttribute('role')).toBe('dialog');
  });

  it('stamps a single <calendar-grid-ui> as the date pane inside the popover', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    const cal = popover.querySelector(':scope > calendar-grid-ui[data-cal-pane]');
    expect(cal).not.toBeNull();
  });

  it('stamps a single <time-picker-ui> as the time pane inside the popover', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    const time = popover.querySelector(':scope > time-picker-ui[data-time-pane]');
    expect(time).not.toBeNull();
  });

  it('stamps a vertical <divider-ui> between the panes', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    const div = popover.querySelector(':scope > divider-ui[data-pane-divider]');
    expect(div).not.toBeNull();
    expect(div.hasAttribute('vertical')).toBe(true);
  });

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

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

  it('uses a custom placeholder when supplied', async () => {
    const el = mount('<datetime-picker-ui placeholder="Pick a moment"></datetime-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toBe('Pick a moment');
  });
});

describe('datetime-picker-ui — value reflection', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('parses ISO datetime value into date + time', async () => {
    const el = mount('<datetime-picker-ui value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    expect(el.datetimeValue).toEqual({ date: '2026-05-24', time: '14:30' });
  });

  it('reflects value into the calendar pane (date portion)', async () => {
    const el = mount('<datetime-picker-ui value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    expect(cal.value).toBe('2026-05-24');
  });

  it('reflects value into the time pane (time portion)', async () => {
    const el = mount('<datetime-picker-ui value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.value).toBe('14:30');
  });

  it('renders formatted datetime in the trigger when value is set', async () => {
    const el = mount('<datetime-picker-ui value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toContain('May 24, 2026');
    expect(trigger.getAttribute('text')).toContain('14:30');
  });

  it('format="long" renders the long-form month', async () => {
    const el = mount('<datetime-picker-ui format="long" value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toContain('May 24, 2026');
  });

  it('format="iso" renders the raw ISO date portion', async () => {
    const el = mount('<datetime-picker-ui format="iso" value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    expect(trigger.getAttribute('text')).toContain('2026-05-24');
  });

  it('reflects [min] onto the calendar pane (date portion)', async () => {
    const el = mount('<datetime-picker-ui min="2026-01-01T00:00"></datetime-picker-ui>');
    await tick();
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    expect(cal.getAttribute('min')).toBe('2026-01-01');
  });

  it('reflects [max] onto the calendar pane (date portion)', async () => {
    const el = mount('<datetime-picker-ui max="2026-12-31T23:59"></datetime-picker-ui>');
    await tick();
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    expect(cal.getAttribute('max')).toBe('2026-12-31');
  });

  it('reflects [precision] onto the time pane', async () => {
    const el = mount('<datetime-picker-ui precision="second"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.getAttribute('precision')).toBe('second');
  });

  it('precision="second" stamps a seconds segment in the time pane', async () => {
    const el = mount('<datetime-picker-ui precision="second" value="2026-05-24T14:30:45"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.querySelector('[data-segment="second"]')).not.toBeNull();
  });

  it('hour-cycle="h12" stamps an AM/PM segment in the time pane', async () => {
    const el = mount('<datetime-picker-ui hour-cycle="h12" value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.querySelector('[data-segment="meridiem"]')).not.toBeNull();
  });

  it('hour-cycle="h23" hides the AM/PM segment in the time pane', async () => {
    const el = mount('<datetime-picker-ui hour-cycle="h23" value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.querySelector('[data-segment="meridiem"]')).toBeNull();
  });

  it('reflects [step] onto the time pane', async () => {
    const el = mount('<datetime-picker-ui step="900"></datetime-picker-ui>');
    await tick();
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    expect(time.getAttribute('step')).toBe('900');
  });
});

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

  it('opens on trigger click + emits `open` event', async () => {
    const el = mount('<datetime-picker-ui></datetime-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('<datetime-picker-ui></datetime-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('<datetime-picker-ui></datetime-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 with trigger=keyboard', async () => {
    const el = mount('<datetime-picker-ui></datetime-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('<datetime-picker-ui disabled></datetime-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('<datetime-picker-ui readonly></datetime-picker-ui>');
    await tick();
    const trigger = el.querySelector(':scope > [slot="trigger"]');
    trigger.click();
    await tick();
    expect(el.open).toBe(false);
  });

  it('openPopover() opens programmatically with trigger=programmatic', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    let openDetail = null;
    el.addEventListener('open', (e) => { openDetail = e.detail; });
    el.openPopover();
    await tick();
    expect(el.open).toBe(true);
    expect(openDetail?.trigger).toBe('programmatic');
  });
});

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

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

  it('commits `change` when both panes have selections', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    cal.value = '2026-03-01';
    cal.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
    await tick();
    time.value = '14:30';
    time.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '14:30', segment: 'minute' } }));
    await tick();
    expect(detail).not.toBeNull();
    expect(detail.value).toBe('2026-03-01T14:30');
    expect(el.value).toBe('2026-03-01T14:30');
  });

  it('time-pane change with existing date in value commits without re-picking date', async () => {
    const el = mount('<datetime-picker-ui value="2026-03-01T09:00"></datetime-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    time.value = '14:30';
    time.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '14:30', segment: 'hour' } }));
    await tick();
    expect(detail.value).toBe('2026-03-01T14:30');
  });

  it('out-of-range commit fires `invalid`, not `change`', async () => {
    const el = mount('<datetime-picker-ui min="2026-06-01T00:00" max="2026-06-30T23:59"></datetime-picker-ui>');
    await tick();
    let invalidDetail = null;
    let changeFired = false;
    el.addEventListener('invalid', (e) => { invalidDetail = e.detail; });
    el.addEventListener('change', () => { changeFired = true; });
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    cal.value = '2026-05-01';
    cal.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-05-01' } }));
    await tick();
    time.value = '12:00';
    time.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '12:00', segment: 'hour' } }));
    await tick();
    expect(invalidDetail).not.toBeNull();
    expect(invalidDetail.reason).toBe('below-min');
    expect(changeFired).toBe(false);
  });

  it('precision="second" emits HH:mm:ss-formatted value on commit', async () => {
    const el = mount('<datetime-picker-ui precision="second" step="1"></datetime-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    cal.value = '2026-03-01';
    cal.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
    await tick();
    time.value = '14:30:45';
    time.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '14:30:45', segment: 'second' } }));
    await tick();
    expect(detail.value).toBe('2026-03-01T14:30:45');
  });

  it('clear() resets the value', async () => {
    const el = mount('<datetime-picker-ui value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    el.clear();
    await tick();
    expect(el.value).toBe('');
    expect(el.datetimeValue).toBeNull();
  });

  it('change event carries source field', async () => {
    const el = mount('<datetime-picker-ui></datetime-picker-ui>');
    await tick();
    let detail = null;
    el.addEventListener('change', (e) => { detail = e.detail; });
    const cal = el.querySelector('calendar-grid-ui[data-cal-pane]');
    const time = el.querySelector('time-picker-ui[data-time-pane]');
    cal.value = '2026-03-01';
    cal.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
    await tick();
    time.value = '14:30';
    time.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '14:30', segment: 'hour' } }));
    await tick();
    expect(detail.source).toBe('click');
  });
});

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

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

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

  it('syncValue writes the ISO datetime to the form', async () => {
    const el = mount('<datetime-picker-ui name="appointment" value="2026-05-24T14:30"></datetime-picker-ui>');
    await tick();
    expect(() => el.syncValue('2026-05-24T14:30')).not.toThrow();
  });

  it('invalid datetime string does not throw under syncValue', async () => {
    const el = mount('<datetime-picker-ui name="appointment"></datetime-picker-ui>');
    await tick();
    expect(() => el.syncValue('not-a-datetime')).not.toThrow();
  });
});

describe('datetime-picker-ui — slots', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('consumer-supplied [slot="trigger"] is preserved', async () => {
    const el = mount(`
      <datetime-picker-ui>
        <button-ui slot="trigger" variant="primary" text="Pick"></button-ui>
      </datetime-picker-ui>
    `);
    await tick();
    const triggers = el.querySelectorAll(':scope > [slot="trigger"]');
    expect(triggers.length).toBe(1);
    expect(triggers[0].getAttribute('variant')).toBe('primary');
  });

  it('consumer-supplied [slot="footer"] is lifted into the popover', async () => {
    const el = mount(`
      <datetime-picker-ui>
        <div slot="footer">
          <button-ui data-action="cancel" text="Cancel"></button-ui>
          <button-ui data-action="apply" variant="primary" text="Apply"></button-ui>
        </div>
      </datetime-picker-ui>
    `);
    await tick();
    const popover = el.querySelector(':scope > [slot="popover"]');
    const footer = popover.querySelector(':scope > [slot="footer"]');
    expect(footer).not.toBeNull();
    expect(footer.children.length).toBe(2);
  });
});
