/**
 * UIElement attribute-driven trait composition.
 *
 *   <comp-ui traits="pressable scale-press ripple">
 *
 * Declarative wiring is the third path next to:
 *   - static traits = [...]   (compile-time, on the class)
 *   - el.addTrait(traitFn)    (runtime, programmatic)
 *
 * Tests assert: connect-time application, idempotence with static, attribute
 * change re-applies, unknown name warns without throwing.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { UIElement } from '../core/element.js';
import { pressable } from './pressable/pressable.js';
import { focusable } from './focusable/focusable.js';
import { hoverable } from './hoverable/hoverable.js';
import { resetDOM } from './test-helpers.js';

let counter = 0;
function defineUniqueTag(BaseImpl) {
  const tag = `decl-trait-test-${++counter}`;
  customElements.define(tag, BaseImpl);
  return tag;
}

describe('UIElement — declarative traits attribute', () => {
  beforeEach(resetDOM);

  it('applies traits listed in the attribute on connect', () => {
    class El extends UIElement {}
    const tag = defineUniqueTag(El);
    document.body.innerHTML = `<${tag} traits="pressable focusable"></${tag}>`;
    const el = document.body.firstElementChild;
    expect(el.hasAttribute('traits')).toBe(true);
    el.dispatchEvent(new PointerEvent('pointerdown'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
    el.dispatchEvent(new FocusEvent('focus'));
    expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
  });

  it('static traits + attribute traits coexist, no double-apply', () => {
    class El extends UIElement {
      static get traits() { return [pressable]; }
    }
    const tag = defineUniqueTag(El);
    document.body.innerHTML = `<${tag} traits="pressable focusable"></${tag}>`;
    const el = document.body.firstElementChild;
    el.dispatchEvent(new PointerEvent('pointerdown'));
    el.dispatchEvent(new PointerEvent('pointerup'));
    let pressCount = 0;
    el.addEventListener('press', () => pressCount++);
    el.dispatchEvent(new PointerEvent('pointerdown'));
    el.dispatchEvent(new PointerEvent('pointerup'));
    // exactly ONE press fires per pointerdown/up (not two from double-apply)
    expect(pressCount).toBe(1);
    // focusable from attribute also wired
    el.dispatchEvent(new FocusEvent('focus'));
    expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
  });

  it('unknown trait name warns but does not throw', () => {
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    class El extends UIElement {}
    const tag = defineUniqueTag(El);
    expect(() => {
      document.body.innerHTML = `<${tag} traits="pressable nonexistent-trait"></${tag}>`;
    }).not.toThrow();
    const el = document.body.firstElementChild;
    // Real trait still applied
    el.dispatchEvent(new PointerEvent('pointerdown'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
    expect(warn).toHaveBeenCalled();
    expect(warn.mock.calls[0][0]).toContain('nonexistent-trait');
    warn.mockRestore();
  });

  it('changing the attribute swaps declarative traits', () => {
    class El extends UIElement {}
    const tag = defineUniqueTag(El);
    document.body.innerHTML = `<${tag} traits="pressable"></${tag}>`;
    const el = document.body.firstElementChild;
    el.dispatchEvent(new PointerEvent('pointerdown'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
    el.dispatchEvent(new PointerEvent('pointerup'));

    // Swap to focusable+hoverable
    el.setAttribute('traits', 'focusable hoverable');

    // pressable should no longer respond
    el.dispatchEvent(new PointerEvent('pointerdown'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(false);

    // focusable should respond
    el.dispatchEvent(new FocusEvent('focus'));
    expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);

    // hoverable should respond
    el.dispatchEvent(new PointerEvent('pointerenter'));
    expect(el.hasAttribute('data-hoverable-hover')).toBe(true);
  });

  it('clearing the attribute removes declarative traits', () => {
    class El extends UIElement {}
    const tag = defineUniqueTag(El);
    document.body.innerHTML = `<${tag} traits="pressable"></${tag}>`;
    const el = document.body.firstElementChild;
    el.removeAttribute('traits');
    el.dispatchEvent(new PointerEvent('pointerdown'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(false);
  });

  it('disconnect cleans up both static and declarative traits', () => {
    class El extends UIElement {
      static get traits() { return [pressable]; }
    }
    const tag = defineUniqueTag(El);
    document.body.innerHTML = `<${tag} traits="focusable"></${tag}>`;
    const el = document.body.firstElementChild;
    el.dispatchEvent(new PointerEvent('pointerdown'));
    el.dispatchEvent(new FocusEvent('focus'));
    expect(el.hasAttribute('data-pressable-pressed')).toBe(true);
    expect(el.hasAttribute('data-focusable-keyboard')).toBe(true);
    el.remove();
    expect(el.hasAttribute('data-pressable-pressed')).toBe(false);
    expect(el.hasAttribute('data-focusable-keyboard')).toBe(false);
  });

  it('observedAttributes always includes "traits"', () => {
    class El extends UIElement {
      static get properties() { return { value: { type: String, default: '' } }; }
    }
    const observed = El.observedAttributes;
    expect(observed).toContain('value');
    expect(observed).toContain('traits');
  });
});
