import { describe, it, expect, beforeEach } from 'vitest';
import { UIElement, signal, effect } from './element.js';

// ── Shared helpers ──

let registrationCounter = 0;
function registerTestElement(BaseImpl) {
  const tag = `test-el-${++registrationCounter}`;
  customElements.define(tag, BaseImpl);
  return tag;
}

function mount(tag, attrs = {}) {
  const el = document.createElement(tag);
  for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
  document.body.appendChild(el);
  return el;
}

// Signals flush via queueMicrotask — await one tick to let reactive
// effects settle after a mutation.
const tick = () => new Promise((r) => queueMicrotask(r));

describe('UIElement — construction + lifecycle', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('instantiates and calls connected() on mount, disconnected() on remove', () => {
    const events = [];
    class El extends UIElement {
      connected()    { events.push('connected'); }
      disconnected() { events.push('disconnected'); }
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    expect(events).toEqual(['connected']);
    el.remove();
    expect(events).toEqual(['connected', 'disconnected']);
  });

  it('remount after disconnect re-runs connected()', () => {
    const events = [];
    class El extends UIElement {
      connected()    { events.push('connected'); }
      disconnected() { events.push('disconnected'); }
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    el.remove();
    document.body.appendChild(el);
    expect(events).toEqual(['connected', 'disconnected', 'connected']);
  });
});

describe('UIElement — properties', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('reads default value from static properties', () => {
    class El extends UIElement {
      static properties = { label: { type: String, default: 'hi' } };
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    expect(el.label).toBe('hi');
  });

  it('reflects Boolean properties to HTML attributes when reflect: true', () => {
    class El extends UIElement {
      static properties = { disabled: { type: Boolean, default: false, reflect: true } };
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    expect(el.hasAttribute('disabled')).toBe(false);
    el.disabled = true;
    expect(el.hasAttribute('disabled')).toBe(true);
    el.disabled = false;
    expect(el.hasAttribute('disabled')).toBe(false);
  });

  it('reflects String properties as attribute values', () => {
    class El extends UIElement {
      static properties = { variant: { type: String, default: 'neutral', reflect: true } };
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    el.variant = 'accent';
    expect(el.getAttribute('variant')).toBe('accent');
  });

  it('parses incoming attributes back into typed properties', () => {
    class El extends UIElement {
      static properties = {
        count:    { type: Number,  default: 0 },
        active:   { type: Boolean, default: false },
      };
    }
    const tag = registerTestElement(El);
    const el = mount(tag, { count: '42', active: '' });
    expect(el.count).toBe(42);
    expect(el.active).toBe(true);
  });

  it('does NOT reflect when reflect: false / omitted', () => {
    class El extends UIElement {
      static properties = { internal: { type: String, default: 'x' } };
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    el.internal = 'y';
    expect(el.hasAttribute('internal')).toBe(false);
  });
});

describe('UIElement — reactive effect', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('calls render() on mount and when a reactive property changes', async () => {
    let renders = 0;
    class El extends UIElement {
      static properties = { label: { type: String, default: 'a' } };
      render() { renders++; void this.label; }
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    expect(renders).toBeGreaterThan(0);
    const before = renders;
    el.label = 'b';
    await tick();
    expect(renders).toBeGreaterThan(before);
  });

  it('stops re-rendering after disconnect', async () => {
    let renders = 0;
    class El extends UIElement {
      static properties = { label: { type: String, default: 'a' } };
      render() { renders++; void this.label; }
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    el.remove();
    const frozen = renders;
    el.label = 'c';
    await tick();
    expect(renders).toBe(frozen);
  });
});

describe('UIElement — subscription-leak guard (0.0.10 regression)', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('child connected() reads do NOT subscribe an outer effect', async () => {
    // The bug: when a parent effect called appendChild(child) while running,
    // child.connectedCallback would synchronously call connected(), and any
    // template-literal reads of child's reactive properties would subscribe
    // the OUTER effect to the CHILD's signals. A later change to a child
    // prop would spuriously re-run the parent effect.
    //
    // Fix: connected() is now wrapped in untracked(…).
    class Child extends UIElement {
      static properties = { value: { type: String, default: 'initial', reflect: true } };
      connected() {
        // Read our own reactive prop exactly the way template-literal
        // interpolation would — this read must not subscribe an ancestor
        // effect.
        void this.value;
      }
    }
    const tag = registerTestElement(Child);

    const trigger = signal(0);
    let outerRuns = 0;
    let childRef = null;

    const dispose = effect(() => {
      outerRuns++;
      void trigger.value;
      if (!childRef) {
        childRef = document.createElement(tag);
        document.body.appendChild(childRef);
      }
    });

    const outerRunsAfterMount = outerRuns;
    expect(childRef).not.toBeNull();

    // Now mutate the CHILD's signal — the outer effect MUST NOT re-run.
    childRef.value = 'changed';
    await tick();
    expect(outerRuns).toBe(outerRunsAfterMount);

    // Sanity check: mutating the trigger signal the outer effect DID
    // subscribe to re-runs the effect, proving the effect is alive.
    trigger.value = 1;
    await tick();
    expect(outerRuns).toBe(outerRunsAfterMount + 1);

    dispose();
  });
});

describe('UIElement — addTrait', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('applies a trait once, idempotent on repeat', () => {
    let connects = 0;
    const myTrait = () => ({
      connect() { connects++; },
      disconnect() {},
    });

    class El extends UIElement {}
    const tag = registerTestElement(El);
    const el = mount(tag);
    el.addTrait(myTrait);
    el.addTrait(myTrait);
    expect(connects).toBe(1);
  });

  it('disconnects all applied traits on remove', () => {
    let connects = 0, disconnects = 0;
    const myTrait = () => ({
      connect()    { connects++; },
      disconnect() { disconnects++; },
    });
    class El extends UIElement {
      static traits = [myTrait];
    }
    const tag = registerTestElement(El);
    const el = mount(tag);
    expect(connects).toBe(1);
    expect(disconnects).toBe(0);
    el.remove();
    expect(disconnects).toBe(1);
  });
});
