/**
 * Universal trait contract — runs against every trait imported from
 * traits/index.js. Asserts schema integrity, attribute cleanup on
 * disconnect, and idempotent connect/disconnect cycles.
 *
 * Per-trait behavior tests live in `<trait>.test.js` siblings.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as traits from './index.js';
import { mountHost, connectTrait, expectValidSchema, resetDOM } from './test-helpers.js';

const KNOWN_CATEGORIES = new Set([
  'input-interaction',
  'keyboard-navigation',
  'forms-data',
  'layout-measurement',
  'motion-positioning',
  'animation-feedback',
  'visual-dynamics',
  'interaction-delight',
  'audio-haptics-sensory',
]);

const ALL_TRAITS = Object.entries(traits).filter(([, fn]) => typeof fn === 'function' && fn.schema);

describe('trait registry — universal contract', () => {
  beforeEach(resetDOM);

  it('exports at least 40 traits', () => {
    expect(ALL_TRAITS.length).toBeGreaterThanOrEqual(40);
  });

  it('every trait declares a known category', () => {
    for (const [exportName, trait] of ALL_TRAITS) {
      expect(KNOWN_CATEGORIES.has(trait.schema.category), `${exportName} category=${trait.schema.category}`).toBe(true);
    }
  });

  it('every trait has a non-empty description', () => {
    for (const [exportName, trait] of ALL_TRAITS) {
      expect(trait.schema.description, `${exportName} missing description`).toBeTruthy();
      expect(trait.schema.description.length, `${exportName} description too short`).toBeGreaterThan(10);
    }
  });

  it('every trait passes the schema-shape contract', () => {
    for (const [, trait] of ALL_TRAITS) expectValidSchema(trait);
  });

  it('attribute, event, and config names use kebab-case', () => {
    const KEBAB_OR_DATA = /^(?:data-)?[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
    for (const [exportName, trait] of ALL_TRAITS) {
      for (const a of trait.schema.attributes) {
        expect(KEBAB_OR_DATA.test(a), `${exportName} attribute "${a}" not kebab-case`).toBe(true);
        expect(a.startsWith('data-'), `${exportName} attribute "${a}" must start with data-`).toBe(true);
      }
      for (const e of trait.schema.events) {
        expect(KEBAB_OR_DATA.test(e), `${exportName} event "${e}" not kebab-case`).toBe(true);
      }
      for (const c of trait.schema.config) {
        expect(c.startsWith('data-'), `${exportName} config "${c}" must start with data-`).toBe(true);
      }
    }
  });

  it('connect → disconnect leaves no managed attributes behind', () => {
    for (const [exportName, trait] of ALL_TRAITS) {
      const host = mountHost();
      const inst = connectTrait(trait, host);
      inst.disconnect(host);
      for (const attr of trait.schema.attributes) {
        expect(host.hasAttribute(attr), `${exportName} left "${attr}" after disconnect`).toBe(false);
      }
      host.remove();
    }
  });

  it('connect → disconnect → connect again does not throw', () => {
    for (const [exportName, trait] of ALL_TRAITS) {
      const host = mountHost();
      const inst1 = connectTrait(trait, host);
      inst1.disconnect(host);
      expect(() => {
        const inst2 = connectTrait(trait, host);
        inst2.disconnect(host);
      }, `${exportName} reconnect threw`).not.toThrow();
      host.remove();
    }
  });

  it('two parallel instances on different hosts do not interfere', () => {
    for (const [exportName, trait] of ALL_TRAITS) {
      const a = mountHost();
      const b = mountHost();
      const instA = connectTrait(trait, a);
      const instB = connectTrait(trait, b);
      // Disconnect one — the other should still clean up correctly.
      expect(() => instA.disconnect(a), `${exportName} disconnect-A threw`).not.toThrow();
      expect(() => instB.disconnect(b), `${exportName} disconnect-B threw`).not.toThrow();
      a.remove();
      b.remove();
    }
  });

  it('catalog count matches export count', async () => {
    const mod = await import('./_catalog.json', { with: { type: 'json' } });
    const catalog = mod.default || mod;
    expect(catalog.count, `catalog says ${catalog.count}, exports has ${ALL_TRAITS.length}`).toBe(ALL_TRAITS.length);
  });
});
