import { describe, it, expect, beforeEach } from 'vitest';
import { announcer } from './announcer.js';
import { _resetRegions, getRegion } from './announcer-stage.js';
import { mountHost, connectTrait, spyEvent, resetDOM, tick, wait } from '../test-helpers.js';

// happy-dom doesn't speak — we assert region textContent instead.
async function settleAnnounce() {
  // The stage's blank-then-write uses queueMicrotask. Two ticks lets
  // both the blank + the message land before we read.
  await tick();
  await tick();
}

describe('announcer', () => {
  beforeEach(() => {
    resetDOM();
    _resetRegions();
  });

  it('schema: declares announcement-made event + four config attrs', () => {
    expect(announcer.schema.name).toBe('announcer');
    expect(announcer.schema.category).toBe('audio-haptics-sensory');
    expect(announcer.schema.events).toContain('announcement-made');
    expect(announcer.schema.config).toEqual([
      'data-announce-on',
      'data-announce-message',
      'data-announce-priority',
      'data-announce-throttle',
    ]);
  });

  it('flips data-announcer-active on connect and cleans up on disconnect', () => {
    const host = mountHost('div', { 'data-announce-on': 'click' });
    const inst = connectTrait(announcer, host);
    expect(host.hasAttribute('data-announcer-active')).toBe(true);
    inst.disconnect(host);
    expect(host.hasAttribute('data-announcer-active')).toBe(false);
  });

  it('with no data-announce-on: trait connects + disconnects without listening', () => {
    const host = mountHost();
    const inst = connectTrait(announcer, host);
    expect(host.hasAttribute('data-announcer-active')).toBe(true);
    // Firing some random event should not crash and not announce anything.
    host.dispatchEvent(new CustomEvent('foo', { detail: 'bar' }));
    expect(document.getElementById('adia-live-polite')).toBeNull();
    inst.disconnect(host);
  });

  it('writes data-announce-message into the polite region by default', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'submit-success',
      'data-announce-message': 'Form submitted successfully.',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('submit-success', { bubbles: true }));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region).not.toBeNull();
    expect(region.getAttribute('aria-live')).toBe('polite');
    expect(region.textContent).toBe('Form submitted successfully.');
    // Assertive region should NOT exist when only polite messages have fired.
    expect(document.getElementById('adia-live-assertive')).toBeNull();
  });

  it('routes data-announce-priority="assertive" to the assertive region', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'invalid',
      'data-announce-message': 'Email is required.',
      'data-announce-priority': 'assertive',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('invalid', { bubbles: true }));
    await settleAnnounce();
    const assertiveRegion = document.getElementById('adia-live-assertive');
    expect(assertiveRegion).not.toBeNull();
    expect(assertiveRegion.getAttribute('aria-live')).toBe('assertive');
    expect(assertiveRegion.getAttribute('role')).toBe('alert');
    expect(assertiveRegion.textContent).toBe('Email is required.');
  });

  it('substitutes {detail} placeholder from a scalar event detail', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'count-up-done',
      'data-announce-message': 'Counted to {detail}.',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('count-up-done', { detail: 18931, bubbles: true }));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region.textContent).toBe('Counted to 18931.');
  });

  it('substitutes {detail} placeholder from object detail.message', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'validated',
      'data-announce-message': 'Validation: {detail}.',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('validated', {
      detail: { valid: false, message: 'Invalid email address' },
      bubbles: true,
    }));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region.textContent).toBe('Validation: Invalid email address.');
  });

  it('falls back to event.detail.message when no template is set', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'validated',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('validated', {
      detail: { message: 'Required field is empty' },
      bubbles: true,
    }));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region.textContent).toBe('Required field is empty');
  });

  it('falls back to data-validation-message when no detail and no template', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'validated',
      'data-validation-message': 'This field is required',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('validated', { bubbles: true }));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region.textContent).toBe('This field is required');
  });

  it('skips announce when there is no resolvable message', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'noop',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    const spy = spyEvent(host, 'announcement-made');
    host.dispatchEvent(new CustomEvent('noop', { bubbles: true }));
    await settleAnnounce();
    expect(spy.count).toBe(0);
    // No region created either — nothing to write into.
    const region = document.getElementById('adia-live-polite');
    expect(region == null || region.textContent === '').toBe(true);
  });

  it('throttle: suppresses a second announce inside the window', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'tick',
      'data-announce-message': 'Tick at {detail}',
      'data-announce-throttle': '1000',
    });
    connectTrait(announcer, host);
    const spy = spyEvent(host, 'announcement-made');
    host.dispatchEvent(new CustomEvent('tick', { detail: 1, bubbles: true }));
    host.dispatchEvent(new CustomEvent('tick', { detail: 2, bubbles: true }));
    host.dispatchEvent(new CustomEvent('tick', { detail: 3, bubbles: true }));
    await settleAnnounce();
    expect(spy.count).toBe(1);
    expect(spy.last.message).toBe('Tick at 1');
  });

  it('throttle=0 lets every event through', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'tick',
      'data-announce-message': 'Tick at {detail}',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    const spy = spyEvent(host, 'announcement-made');
    host.dispatchEvent(new CustomEvent('tick', { detail: 1 }));
    host.dispatchEvent(new CustomEvent('tick', { detail: 2 }));
    await settleAnnounce();
    expect(spy.count).toBe(2);
  });

  it('dispatches announcement-made with { message, priority } detail', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'go',
      'data-announce-message': 'Hello',
      'data-announce-priority': 'assertive',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    const spy = spyEvent(host, 'announcement-made');
    host.dispatchEvent(new CustomEvent('go', { bubbles: true }));
    await settleAnnounce();
    expect(spy.count).toBe(1);
    expect(spy.last.message).toBe('Hello');
    expect(spy.last.priority).toBe('assertive');
  });

  it('singleton regions: two announcer instances share one polite region', async () => {
    const a = mountHost('div', {
      'data-announce-on': 'go',
      'data-announce-message': 'A says hi',
      'data-announce-throttle': '0',
    });
    const b = mountHost('div', {
      'data-announce-on': 'go',
      'data-announce-message': 'B says hi',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, a);
    connectTrait(announcer, b);
    a.dispatchEvent(new CustomEvent('go'));
    await settleAnnounce();
    b.dispatchEvent(new CustomEvent('go'));
    await settleAnnounce();
    // Only one polite region in the document.
    const regions = document.querySelectorAll('#adia-live-polite');
    expect(regions.length).toBe(1);
    expect(regions[0].textContent).toBe('B says hi');
  });

  it('regions are created with sr-only clipping (visually hidden, AT-visible)', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'go',
      'data-announce-message': 'hi',
      'data-announce-throttle': '0',
    });
    connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('go'));
    await settleAnnounce();
    const region = document.getElementById('adia-live-polite');
    expect(region.classList.contains('adia-sr-only')).toBe(true);
    expect(region.getAttribute('aria-atomic')).toBe('true');
    // The role is 'status' for polite, 'alert' for assertive.
    expect(region.getAttribute('role')).toBe('status');
  });

  it('disconnect leaves the singleton regions in place for other consumers', () => {
    const host = mountHost('div', { 'data-announce-on': 'go' });
    // Pre-create the region by hand so we know it's there before connect.
    getRegion('polite');
    const inst = connectTrait(announcer, host);
    inst.disconnect(host);
    expect(document.getElementById('adia-live-polite')).not.toBeNull();
  });

  it('reconnect after disconnect re-listens cleanly', async () => {
    const host = mountHost('div', {
      'data-announce-on': 'go',
      'data-announce-message': 'reconnected',
      'data-announce-throttle': '0',
    });
    const inst1 = connectTrait(announcer, host);
    inst1.disconnect(host);
    host.dispatchEvent(new CustomEvent('go'));
    await settleAnnounce();
    expect(document.getElementById('adia-live-polite')?.textContent || '').toBe('');

    const inst2 = connectTrait(announcer, host);
    host.dispatchEvent(new CustomEvent('go'));
    await settleAnnounce();
    expect(document.getElementById('adia-live-polite').textContent).toBe('reconnected');
    inst2.disconnect(host);
  });
});
