/**
 * alert-ui tests
 *
 * Three content modes (precedence order):
 *   1. Author-provided <span slot="content">…</span> — wins
 *   2. [title] + [description] — bolded headline + body paragraph
 *   3. [text] — single-line message
 *
 * Two alias categories:
 *   - First-class aliases (public, no warn): [dismissible] ↔ [closable]
 *   - Hallucination aliases (warn-once): variant="error" → "danger";
 *     [closeable] → [closable]
 */

import { describe, it, expect, beforeEach } from 'vitest';
import '../../core/element.js';
import './alert.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('alert-ui — canonical props', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('renders text from the canonical `text` prop', async () => {
    const a = mount('<alert-ui text="Hello" variant="danger"></alert-ui>');
    await tick();
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content).not.toBeNull();
    expect(content.textContent).toBe('Hello');
  });

  it('sets role=alert for danger variant', async () => {
    const a = mount('<alert-ui text="Error" variant="danger"></alert-ui>');
    await tick();
    expect(a.getAttribute('role')).toBe('alert');
  });

  it('sets role=status for non-danger variants', async () => {
    const a = mount('<alert-ui text="Info" variant="info"></alert-ui>');
    await tick();
    expect(a.getAttribute('role')).toBe('status');
  });

  it('renders [title] + [description] as bold-headline + body (first-class, no warn)', async () => {
    const a = mount('<alert-ui title="Heads up" description="A warning was raised." variant="warning"></alert-ui>');
    await tick();
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content).not.toBeNull();
    expect(content.textContent).toContain('Heads up');
    expect(content.textContent).toContain('A warning was raised.');
    const strong = content.querySelector('strong');
    expect(strong).not.toBeNull();
    expect(strong.textContent).toBe('Heads up');
    // No warn for canonical title/description (they're first-class)
    expect(a.getAttribute('role')).toBe('alert'); // warning → alert role
  });

  it('renders [title] alone (description omitted)', async () => {
    const a = mount('<alert-ui title="Heads up" variant="warning"></alert-ui>');
    await tick();
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content).not.toBeNull();
    expect(content.textContent.trim()).toBe('Heads up');
    expect(content.querySelector('strong').textContent).toBe('Heads up');
  });

  it('renders [description] alone (title omitted)', async () => {
    const a = mount('<alert-ui description="A quiet notice." variant="info"></alert-ui>');
    await tick();
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content).not.toBeNull();
    expect(content.textContent.trim()).toBe('A quiet notice.');
    expect(content.querySelector('strong')).toBeNull();
  });

  it('[dismissible] is first-class — same effect as [closable], no warn', async () => {
    const a = mount('<alert-ui text="Banner" dismissible></alert-ui>');
    await tick();
    expect(a.hasAttribute('closable')).toBe(true);
    // dismissible attribute is preserved on the host for forward-compat
    // (consumers can read either)
    expect(a.querySelector('[slot="close"]')).not.toBeNull();
  });

  it('[closable] works canonically without [dismissible]', async () => {
    const a = mount('<alert-ui text="Banner" closable></alert-ui>');
    await tick();
    expect(a.hasAttribute('closable')).toBe(true);
    expect(a.querySelector('[slot="close"]')).not.toBeNull();
  });
});

describe('alert-ui — hallucination aliases (warn-once)', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('maps variant="error" to variant="danger" (warns)', async () => {
    const a = mount('<alert-ui text="Oops" variant="error"></alert-ui>');
    await tick();
    expect(a.getAttribute('variant')).toBe('danger');
    expect(a.getAttribute('role')).toBe('alert');
  });

  it('maps [closeable] (misspelling) to [closable] (warns)', async () => {
    const a = mount('<alert-ui text="Banner" closeable></alert-ui>');
    await tick();
    expect(a.hasAttribute('closable')).toBe(true);
    expect(a.hasAttribute('closeable')).toBe(false);
    expect(a.querySelector('[slot="close"]')).not.toBeNull();
  });
});

describe('alert-ui — full corpus shape (the §34 scenario)', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('renders the exact alert-banner.json corpus pattern correctly', async () => {
    // This is `patterns/agent/alert-banner.json` rendered through the
    // component. All four props are now first-class / aliased; the
    // alert renders with full visible content.
    const a = mount(`<alert-ui
      variant="info"
      icon="info-circle"
      title="System Update"
      description="A new version is available. Please refresh to get the latest features."
      dismissible
    ></alert-ui>`);
    await tick();
    // Variant stays canonical
    expect(a.getAttribute('variant')).toBe('info');
    // Dismissible maps to closable
    expect(a.hasAttribute('closable')).toBe(true);
    // Role is status (info, not danger/warning)
    expect(a.getAttribute('role')).toBe('status');
    // Content composed correctly
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content.textContent).toContain('System Update');
    expect(content.textContent).toContain('A new version is available.');
    expect(content.querySelector('strong').textContent).toBe('System Update');
    // Close button present (dismissible → closable)
    expect(a.querySelector('[slot="close"]')).not.toBeNull();
    // ARIA composed
    expect(a.getAttribute('aria-label')).toContain('System Update');
    expect(a.getAttribute('aria-label')).toContain('A new version is available');
  });

  it('renders the exact destructive-confirm.json corpus pattern correctly', async () => {
    // This is the shape used in compositions/forms/destructive-confirm.json:
    // variant=danger + title + description (no close button)
    const a = mount(`<alert-ui
      variant="danger"
      title="This action is permanent"
      description="Deleting your account cannot be undone. All your data will be removed within 30 days."
    ></alert-ui>`);
    await tick();
    expect(a.getAttribute('variant')).toBe('danger');
    expect(a.getAttribute('role')).toBe('alert');
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content.querySelector('strong').textContent).toBe('This action is permanent');
    expect(content.textContent).toContain('Deleting your account cannot be undone');
  });

  it('preserves author-provided [slot="content"] when [title]+[description] are also set', async () => {
    // Authored rich content wins; canonical props become metadata only
    // (still mirrored to aria-label for screen reader semantics).
    const a = mount(`<alert-ui title="hdr" description="body">
      <span slot="content">Custom <a href="/foo">link</a></span>
    </alert-ui>`);
    await tick();
    const content = a.querySelector(':scope > [slot="content"]');
    // Should preserve the author's link
    expect(content.querySelector('a')).not.toBeNull();
    expect(content.querySelector('a').getAttribute('href')).toBe('/foo');
  });
});

describe('alert-ui — SPEC-006 dunning pattern', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('formats amount via Intl.NumberFormat with currency', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
                               due-at="2026-05-20" card-last4="4242"
                               reason="declined" variant="danger"></alert-ui>`);
    await tick();
    const amt = a.querySelector('[data-dunning-amount]');
    expect(amt).not.toBeNull();
    // en-US default formatting (jsdom): "$24.50"
    expect(amt.textContent).toMatch(/\$24\.50|USD\s*24\.50/);
  });

  it('formats due date via Intl.DateTimeFormat', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
                               due-at="2026-05-20T00:00:00Z"
                               reason="declined" variant="danger"></alert-ui>`);
    await tick();
    const meta = a.querySelector('[data-dunning-meta]');
    expect(meta).not.toBeNull();
    // Expect month name + day + year somewhere in the meta
    expect(meta.textContent).toMatch(/May/);
    expect(meta.textContent).toMatch(/2026/);
  });

  it('stamps the cardLast4 metadata', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
                               due-at="2026-05-20" card-last4="4242"
                               reason="declined" variant="danger"></alert-ui>`);
    await tick();
    const meta = a.querySelector('[data-dunning-meta]');
    expect(meta.textContent).toContain('ending 4242');
  });

  it('stamps x-circle icon for reason="declined"', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
                               amount="10" currency="USD"></alert-ui>`);
    await tick();
    const icon = a.querySelector('[slot="leading"]');
    expect(icon).not.toBeNull();
    expect(icon.getAttribute('name')).toBe('x-circle');
  });

  it('stamps clock icon for reason="expired"', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
                               amount="10" currency="USD"></alert-ui>`);
    await tick();
    const icon = a.querySelector('[slot="leading"]');
    expect(icon.getAttribute('name')).toBe('clock');
  });

  it('dispatches dunning-action event on descendant [data-dunning-action] click', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
                               due-at="2026-05-20" reason="declined" variant="danger">
      <button-ui slot="actions" data-dunning-action="update" text="Update"></button-ui>
      <button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
    </alert-ui>`);
    await tick();

    const received = [];
    a.addEventListener('dunning-action', (e) => received.push(e.detail));

    const updateBtn = a.querySelector('[data-dunning-action="update"]');
    updateBtn.dispatchEvent(new Event('press', { bubbles: true }));
    await tick();

    expect(received.length).toBe(1);
    expect(received[0].action).toBe('update');
    expect(received[0].amount).toBe(24.5);
    expect(received[0].currency).toBe('USD');
    expect(received[0].dueAt).toBe('2026-05-20');
  });

  it('dispatches dunning-action with action="retry" when retry button is pressed', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
                               due-at="2026-05-20" reason="declined" variant="danger">
      <button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
    </alert-ui>`);
    await tick();

    const received = [];
    a.addEventListener('dunning-action', (e) => received.push(e.detail));

    const retryBtn = a.querySelector('[data-dunning-action="retry"]');
    retryBtn.dispatchEvent(new Event('press', { bubbles: true }));
    await tick();

    expect(received.length).toBe(1);
    expect(received[0].action).toBe('retry');
  });

  it('re-formats amount when the [amount] attribute changes', async () => {
    const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
                               due-at="2026-05-20" reason="declined" variant="danger"></alert-ui>`);
    await tick();
    let amt = a.querySelector('[data-dunning-amount]');
    expect(amt.textContent).toMatch(/10/);

    a.amount = 99.99;
    await tick();
    amt = a.querySelector('[data-dunning-amount]');
    expect(amt.textContent).toMatch(/99\.99/);
  });

  it('inherits role="alert" for variant="danger" in dunning mode', async () => {
    const a = mount(`<alert-ui pattern="dunning" variant="danger" amount="10" currency="USD"
                               due-at="2026-05-20" reason="declined"></alert-ui>`);
    await tick();
    expect(a.getAttribute('role')).toBe('alert');
  });

  it('inherits role="alert" for variant="warning" (grace period)', async () => {
    const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
                               due-at="2026-06-01"></alert-ui>`);
    await tick();
    // warning maps to role=alert (same as danger via #updateRole)
    expect(a.getAttribute('role')).toBe('alert');
  });

  it('grace-period (warning) uses "due" prefix in date meta, not "failed"', async () => {
    const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
                               due-at="2026-06-01"></alert-ui>`);
    await tick();
    const meta = a.querySelector('[data-dunning-meta]');
    expect(meta.textContent).toContain('due');
  });

  it('default title for declined is "Payment failed"', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
                               amount="10" currency="USD"></alert-ui>`);
    await tick();
    const titleEl = a.querySelector('[data-dunning-title]');
    expect(titleEl.textContent).toBe('Payment failed');
  });

  it('default title for expired is "Card expired"', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
                               amount="10" currency="USD"></alert-ui>`);
    await tick();
    const titleEl = a.querySelector('[data-dunning-title]');
    expect(titleEl.textContent).toBe('Card expired');
  });

  it('explicit [title] overrides the per-reason default', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="declined" title="Custom message"
                               amount="10" currency="USD" variant="danger"></alert-ui>`);
    await tick();
    const titleEl = a.querySelector('[data-dunning-title]');
    expect(titleEl.textContent).toBe('Custom message');
  });

  it('explicit [icon] overrides the per-reason icon map', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="declined" icon="credit-card"
                               amount="10" currency="USD" variant="danger"></alert-ui>`);
    await tick();
    const icon = a.querySelector('[slot="leading"]');
    expect(icon.getAttribute('name')).toBe('credit-card');
  });

  it('composes aria-label from title + amount + meta', async () => {
    const a = mount(`<alert-ui pattern="dunning" reason="declined" amount="24.50"
                               currency="USD" due-at="2026-05-20" card-last4="4242"
                               variant="danger"></alert-ui>`);
    await tick();
    const aria = a.getAttribute('aria-label');
    expect(aria).toContain('Payment failed');
    expect(aria).toMatch(/24\.50/);
    expect(aria).toMatch(/4242/);
  });

  it('does not stamp dunning content when pattern is empty (standard mode)', async () => {
    const a = mount(`<alert-ui variant="danger" amount="24.50" currency="USD"
                               due-at="2026-05-20" reason="declined"
                               text="standard message"></alert-ui>`);
    await tick();
    // No dunning markers — uses the standard text-only render path
    expect(a.querySelector('[data-dunning-amount]')).toBeNull();
    expect(a.querySelector('[data-dunning-title]')).toBeNull();
    const content = a.querySelector(':scope > [slot="content"]');
    expect(content.textContent.trim()).toBe('standard message');
  });
});
