import { describe, it, expect, beforeEach } from 'vitest';
import { focusRestore } from './focus-restore.js';
import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';

function focusableTrigger(label = 'Trigger') {
  const btn = document.createElement('button');
  btn.textContent = label;
  document.body.appendChild(btn);
  return btn;
}

function focusableChild(host, label = 'child', tag = 'button') {
  const el = document.createElement(tag);
  el.textContent = label;
  if (tag !== 'button') el.setAttribute('tabindex', '0');
  host.appendChild(el);
  return el;
}

describe('focus-restore', () => {
  beforeEach(resetDOM);

  it('connect sets data-focus-restore-active', () => {
    const host = mountHost();
    connectTrait(focusRestore, host);
    expect(host.hasAttribute('data-focus-restore-active')).toBe(true);
  });

  it('disconnect clears data-focus-restore-active', () => {
    const host = mountHost();
    const inst = connectTrait(focusRestore, host);
    inst.disconnect(host);
    expect(host.hasAttribute('data-focus-restore-active')).toBe(false);
  });

  it('captures activeElement on connect and restores it on disconnect', () => {
    const trigger = focusableTrigger('Open');
    trigger.focus();
    expect(document.activeElement).toBe(trigger);

    const host = mountHost();
    const inst = connectTrait(focusRestore, host);

    // Some other element steals focus while the surface is open.
    const distract = focusableTrigger('Distract');
    distract.focus();
    expect(document.activeElement).toBe(distract);

    inst.disconnect(host);

    // Focus snaps back to the originally-captured trigger.
    expect(document.activeElement).toBe(trigger);
  });

  it('falls back to body when the captured target is removed from DOM', () => {
    const trigger = focusableTrigger('Will be removed');
    trigger.focus();

    const host = mountHost();
    const inst = connectTrait(focusRestore, host);

    // Trigger gets nuked while the surface is open — common pattern when
    // the container that opened the modal re-renders.
    trigger.remove();
    expect(trigger.isConnected).toBe(false);

    inst.disconnect(host);

    // Restore must not throw. Focus lands on body (or null) — never the
    // removed trigger. The contract is "don't dangle" not "always lands".
    expect(document.activeElement).not.toBe(trigger);
    expect(document.activeElement === document.body || document.activeElement === null).toBe(true);
  });

  it('falls back when captured target is disabled', () => {
    const trigger = focusableTrigger('Will be disabled');
    trigger.focus();

    const host = mountHost();
    const inst = connectTrait(focusRestore, host);

    // Trigger gets disabled while surface is open.
    trigger.setAttribute('disabled', '');

    inst.disconnect(host);

    // Disabled element shouldn't receive focus — fallback path engages.
    expect(document.activeElement).not.toBe(trigger);
  });

  it('dispatches focus-restored on disconnect with detail.restoredTo + detail.capturedTarget', () => {
    const trigger = focusableTrigger('Open');
    trigger.focus();

    const host = mountHost();
    const inst = connectTrait(focusRestore, host);
    const spy = spyEvent(host, 'focus-restored');

    inst.disconnect(host);

    expect(spy.count).toBe(1);
    expect(spy.last.capturedTarget).toBe(trigger);
    expect(spy.last.restoredTo).toBe(trigger);
  });

  it('data-focus-restore-on-mount="none" (default): does NOT move focus on connect', () => {
    const trigger = focusableTrigger('Stay focused');
    trigger.focus();

    const host = mountHost();
    focusableChild(host, 'inside');
    connectTrait(focusRestore, host);

    // Default behavior — captures, doesn't move focus.
    expect(document.activeElement).toBe(trigger);
  });

  it('data-focus-restore-on-mount="host": moves focus to the host on connect', () => {
    const trigger = focusableTrigger('Open');
    trigger.focus();

    const host = mountHost('div', {
      tabindex: '-1',
      'data-focus-restore-on-mount': 'host',
    });
    connectTrait(focusRestore, host);

    expect(document.activeElement).toBe(host);
  });

  it('data-focus-restore-on-mount="first-focusable": focuses first tabbable descendant', () => {
    const trigger = focusableTrigger('Open');
    trigger.focus();

    const host = mountHost('div', {
      'data-focus-restore-on-mount': 'first-focusable',
    });
    const first = focusableChild(host, 'First');
    focusableChild(host, 'Second');
    connectTrait(focusRestore, host);

    expect(document.activeElement).toBe(first);
  });

  it('first-focusable: skips disabled descendants', () => {
    const trigger = focusableTrigger('Open');
    trigger.focus();

    const host = mountHost('div', {
      'data-focus-restore-on-mount': 'first-focusable',
    });
    const disabled = focusableChild(host, 'Disabled');
    disabled.setAttribute('disabled', '');
    const enabled = focusableChild(host, 'Enabled');
    connectTrait(focusRestore, host);

    expect(document.activeElement).toBe(enabled);
  });

  it('on-mount captures BEFORE moving focus, so disconnect restores correctly', () => {
    // Regression guard: the trait must capture activeElement before
    // its own focus shift, otherwise disconnect would restore to the
    // host (or first child), not the original trigger.
    const trigger = focusableTrigger('Open');
    trigger.focus();

    const host = mountHost('div', {
      tabindex: '-1',
      'data-focus-restore-on-mount': 'host',
    });
    const inst = connectTrait(focusRestore, host);
    expect(document.activeElement).toBe(host);

    inst.disconnect(host);
    expect(document.activeElement).toBe(trigger);
  });

  it('connect → disconnect → connect cycle works', () => {
    const trigger = focusableTrigger('Reusable');
    trigger.focus();

    const host = mountHost();
    const inst1 = connectTrait(focusRestore, host);
    inst1.disconnect(host);
    expect(document.activeElement).toBe(trigger);

    // Refocus and run again — fresh capture, fresh restore.
    const trigger2 = focusableTrigger('Different trigger');
    trigger2.focus();
    const inst2 = connectTrait(focusRestore, host);
    inst2.disconnect(host);
    expect(document.activeElement).toBe(trigger2);
  });

  it('schema declares the documented contract', () => {
    expect(focusRestore.schema.name).toBe('focus-restore');
    expect(focusRestore.schema.category).toBe('keyboard-navigation');
    expect(focusRestore.schema.attributes).toContain('data-focus-restore-active');
    expect(focusRestore.schema.events).toContain('focus-restored');
    expect(focusRestore.schema.config).toContain('data-focus-restore-on-mount');
  });
});
