/**
 * loading-overlay-ui tests — covers SPEC-034 § Verification.
 *
 * The component is JS-driven (delay timer + aria-busy bookkeeping) with
 * a CSS-only fade transition. Tests split:
 *
 *   1. Defaults + property reflection.
 *   2. aria-busy parent wiring (apply on activate, release on
 *      deactivate / disconnect).
 *   3. [delay] grace-window suppression — fast-load no-flash.
 *   4. Auto-stamped spinner-ui (default slot empty) vs slotted content.
 *   5. CSS source contract — two-block @scope, token surface, variants.
 */

import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

const HERE = dirname(fileURLToPath(import.meta.url));
const LO_CSS = readFileSync(resolve(HERE, 'loading-overlay.css'), 'utf8');

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

beforeAll(async () => {
  await import('./loading-overlay.js');
  // Side-effect import so the auto-stamp can construct spinner-ui.
  await import('../spinner/spinner.js');
});

function mountInPositionedParent(html) {
  const parent = document.createElement('div');
  parent.style.position = 'relative';
  parent.style.width = '200px';
  parent.style.height = '120px';
  parent.id = 'lo-parent';
  parent.innerHTML = html;
  document.body.appendChild(parent);
  return { parent, overlay: parent.querySelector('loading-overlay-ui') };
}

// ── 1. Defaults + property reflection ────────────────────────────────

describe('loading-overlay-ui — defaults', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('renders without props using documented defaults', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
    await tick();
    expect(overlay.active).toBe(false);
    expect(overlay.delay).toBe(200);
    expect(overlay.label).toBe('Loading…');
    expect(overlay.variant).toBe('default');
  });

  it('sets role="status" + aria-live="polite" on the host', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
    await tick();
    expect(overlay.getAttribute('role')).toBe('status');
    expect(overlay.getAttribute('aria-live')).toBe('polite');
  });

  it('seeds aria-label from the [label] prop', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Saving"></loading-overlay-ui>');
    await tick();
    expect(overlay.getAttribute('aria-label')).toBe('Saving');
  });

  it('reflects [active], [variant], [delay], [label] to the host attributes', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui active variant="blur" delay="500" label="Wait"></loading-overlay-ui>');
    await tick();
    expect(overlay.hasAttribute('active')).toBe(true);
    expect(overlay.getAttribute('variant')).toBe('blur');
    expect(overlay.getAttribute('delay')).toBe('500');
    expect(overlay.getAttribute('label')).toBe('Wait');
  });
});

// ── 2. aria-busy parent wiring ───────────────────────────────────────

describe('loading-overlay-ui — aria-busy parent wiring', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
    vi.useFakeTimers();
  });
  afterEach(() => { vi.useRealTimers(); });

  it('applies aria-busy="true" to parent on activate (after delay)', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui delay="0"></loading-overlay-ui>');
    await tick();
    const overlay = parent.querySelector('loading-overlay-ui');
    expect(parent.hasAttribute('aria-busy')).toBe(false);
    overlay.active = true;
    // delay=0 → applies synchronously via setTimeout(0)
    vi.advanceTimersByTime(0);
    expect(parent.getAttribute('aria-busy')).toBe('true');
  });

  it('releases aria-busy on deactivate', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
    await tick();
    vi.advanceTimersByTime(0);
    expect(parent.getAttribute('aria-busy')).toBe('true');

    const overlay = parent.querySelector('loading-overlay-ui');
    overlay.active = false;
    expect(parent.hasAttribute('aria-busy')).toBe(false);
  });

  it('releases aria-busy on disconnect', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
    await tick();
    vi.advanceTimersByTime(0);
    expect(parent.getAttribute('aria-busy')).toBe('true');

    const overlay = parent.querySelector('loading-overlay-ui');
    overlay.remove();
    expect(parent.hasAttribute('aria-busy')).toBe(false);
  });

  it('honors mount-time [active] (declarative)', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
    await tick();
    vi.advanceTimersByTime(0);
    expect(parent.getAttribute('aria-busy')).toBe('true');
  });
});

// ── 3. [delay] grace window ──────────────────────────────────────────

describe('loading-overlay-ui — [delay] grace window', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
    vi.useFakeTimers();
  });
  afterEach(() => { vi.useRealTimers(); });

  it('suppresses paint when [active] is cleared before delay expires', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
    await tick();
    const overlay = parent.querySelector('loading-overlay-ui');

    overlay.active = true;
    // Within the grace window — parent should NOT be marked yet.
    vi.advanceTimersByTime(100);
    expect(parent.hasAttribute('aria-busy')).toBe(false);

    // Clear before timer fires.
    overlay.active = false;
    vi.advanceTimersByTime(200);
    expect(parent.hasAttribute('aria-busy')).toBe(false);
  });

  it('paints after delay elapses when [active] stays true', async () => {
    const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
    await tick();
    const overlay = parent.querySelector('loading-overlay-ui');

    overlay.active = true;
    expect(parent.hasAttribute('aria-busy')).toBe(false);
    vi.advanceTimersByTime(200);
    expect(parent.getAttribute('aria-busy')).toBe('true');
  });
});

// ── 4. Default-slot auto-stamp ───────────────────────────────────────

describe('loading-overlay-ui — auto-stamped spinner', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('auto-stamps a <spinner-ui size="lg"> when default slot is empty', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
    await tick();
    const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
    expect(sp).not.toBeNull();
    expect(sp.getAttribute('size')).toBe('lg');
  });

  it('does NOT auto-stamp when consumer slots their own indicator', async () => {
    const { overlay } = mountInPositionedParent(
      '<loading-overlay-ui><skeleton-ui width="100%" height="1rem"></skeleton-ui></loading-overlay-ui>'
    );
    await tick();
    expect(overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]')).toBeNull();
    expect(overlay.querySelector(':scope > skeleton-ui')).not.toBeNull();
  });

  it('forwards [label] changes to the auto-stamped spinner', async () => {
    const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Loading"></loading-overlay-ui>');
    await tick();
    overlay.label = 'Uploading';
    await tick();
    const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
    expect(sp.getAttribute('label')).toBe('Uploading');
  });
});

// ── 5. CSS source contract ───────────────────────────────────────────

describe('loading-overlay-ui — CSS source contract', () => {
  it('opens with the canonical @scope (loading-overlay-ui) block', () => {
    expect(LO_CSS).toMatch(/^@scope \(loading-overlay-ui\)/);
  });

  it('declares both the :where(:scope) token block and a :scope base block', () => {
    expect(LO_CSS).toMatch(/:where\(:scope\)\s*\{/);
    expect(LO_CSS).toMatch(/^\s*:scope\s*\{/m);
  });

  it('declares the documented component tokens', () => {
    for (const tok of [
      '--loading-overlay-bg',
      '--loading-overlay-radius',
      '--loading-overlay-z',
      '--loading-overlay-duration',
      '--loading-overlay-easing',
      '--loading-overlay-gap',
    ]) {
      expect(LO_CSS).toContain(tok);
    }
  });

  it('positions absolutely with inset:0 (fills the parent)', () => {
    expect(LO_CSS).toMatch(/position:\s*absolute/);
    expect(LO_CSS).toMatch(/inset:\s*0/);
  });

  it('idle state: opacity:0 + pointer-events:none', () => {
    // The base :scope block carries both — fail-fast if either is dropped.
    expect(LO_CSS).toMatch(/opacity:\s*0/);
    expect(LO_CSS).toMatch(/pointer-events:\s*none/);
  });

  it('active state: opacity:1 + pointer-events:auto', () => {
    expect(LO_CSS).toMatch(/:scope\[active\][^}]*opacity:\s*1/);
    expect(LO_CSS).toMatch(/:scope\[active\][^}]*pointer-events:\s*auto/);
  });

  it('transparent variant drops the backdrop fill', () => {
    expect(LO_CSS).toMatch(/:scope\[variant="transparent"\][^}]*--loading-overlay-bg:\s*transparent/);
  });

  it('blur variant applies backdrop-filter blur', () => {
    expect(LO_CSS).toMatch(/:scope\[variant="blur"\][^}]*backdrop-filter:\s*blur/);
  });

  it('reduced-motion media query suppresses the fade transition', () => {
    expect(LO_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
    expect(LO_CSS).toMatch(/transition:\s*none/);
  });

  it('uses semantic --a-* tokens; no raw hex / rgb / oklch in the file', () => {
    expect(LO_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
    expect(LO_CSS).not.toMatch(/\brgb\s*\(/);
    expect(LO_CSS).not.toMatch(/\boklch\s*\(/);
  });
});
