import { describe, it, expect, beforeEach } from 'vitest';
import { dropTarget } from './drop-target.js';
import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';

/**
 * happy-dom's DragEvent constructor does NOT preserve the `dataTransfer`
 * field passed via the init dict (verified at authoring time:
 * `new DragEvent('dragover', { dataTransfer })` then `.dataTransfer`
 * comes back as null). We synthesize the event by creating an Event,
 * then defining `dataTransfer` as a non-enumerable read-only property —
 * the trait reads it the same way the browser would.
 */
function fireDragEvent(host, type, dataTransfer = null) {
  const ev = new Event(type, { bubbles: true, cancelable: true });
  Object.defineProperty(ev, 'dataTransfer', {
    value: dataTransfer,
    writable: false,
    configurable: true,
  });
  host.dispatchEvent(ev);
  return ev;
}

function fakeDataTransfer({ types = [], items = [] } = {}) {
  return {
    types,
    items,
    files: items.filter((i) => i.kind === 'file').map((i) => i.file).filter(Boolean),
    getData: () => '',
    setData: () => {},
  };
}

describe('drop-target', () => {
  beforeEach(resetDOM);

  it('sets data-drop-target-active on connect, removes on disconnect', () => {
    const host = mountHost();
    const inst = connectTrait(dropTarget, host);
    expect(host.hasAttribute('data-drop-target-active')).toBe(true);
    inst.disconnect(host);
    expect(host.hasAttribute('data-drop-target-active')).toBe(false);
  });

  it('dragenter sets data-drop-target-over and dispatches drop-enter', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const spy = spyEvent(host, 'drop-enter');
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    expect(host.hasAttribute('data-drop-target-over')).toBe(true);
    expect(spy.count).toBe(1);
  });

  it('dragleave clears data-drop-target-over and dispatches drop-leave', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const enterSpy = spyEvent(host, 'drop-enter');
    const leaveSpy = spyEvent(host, 'drop-leave');
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    fireDragEvent(host, 'dragleave', fakeDataTransfer());
    expect(host.hasAttribute('data-drop-target-over')).toBe(false);
    expect(enterSpy.count).toBe(1);
    expect(leaveSpy.count).toBe(1);
  });

  it('dragenter from descendant nesting only emits one drop-enter', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const enterSpy = spyEvent(host, 'drop-enter');
    const leaveSpy = spyEvent(host, 'drop-leave');
    // Two enters (host then descendant) should produce ONE drop-enter
    // and NOT clear the over-state when one matching leave fires.
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    fireDragEvent(host, 'dragleave', fakeDataTransfer());
    expect(enterSpy.count).toBe(1);
    expect(leaveSpy.count).toBe(0);
    expect(host.hasAttribute('data-drop-target-over')).toBe(true);
    // The matched leave finally clears.
    fireDragEvent(host, 'dragleave', fakeDataTransfer());
    expect(leaveSpy.count).toBe(1);
    expect(host.hasAttribute('data-drop-target-over')).toBe(false);
  });

  it('dragover calls preventDefault when no accept filter is set', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const ev = fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['text/plain'] }));
    expect(ev.defaultPrevented).toBe(true);
  });

  it('dragover with empty accept attribute still accepts (preventDefault)', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': '' });
    connectTrait(dropTarget, host);
    const ev = fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['text/plain'] }));
    expect(ev.defaultPrevented).toBe(true);
  });

  it('accepts="files" — accepts file drags, rejects non-file payloads', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': 'files' });
    connectTrait(dropTarget, host);
    const rejectedSpy = spyEvent(host, 'drop-rejected');

    // File drag — Files token present → accepted
    const fileEv = fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['Files'] }));
    expect(fileEv.defaultPrevented).toBe(true);
    expect(host.hasAttribute('data-drop-target-rejected')).toBe(false);

    // Plain text drag — no Files → rejected, no preventDefault
    const textEv = fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['text/plain'] }));
    expect(textEv.defaultPrevented).toBe(false);
    expect(host.getAttribute('data-drop-target-rejected')).toBe('files-only');
    expect(rejectedSpy.count).toBe(1);
    expect(rejectedSpy.last.reason).toBe('files-only');
  });

  it('accepts="image/png,image/jpeg" — rejects mismatched MIME types', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': 'image/png,image/jpeg' });
    connectTrait(dropTarget, host);

    // Direct text type → rejected
    const textEv = fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['text/plain'] }));
    expect(textEv.defaultPrevented).toBe(false);
    expect(host.getAttribute('data-drop-target-rejected')).toBe('type');

    // PNG file drag → accepted
    const pngDt = fakeDataTransfer({
      types: ['Files'],
      items: [{ kind: 'file', type: 'image/png' }],
    });
    const pngEv = fireDragEvent(host, 'dragover', pngDt);
    expect(pngEv.defaultPrevented).toBe(true);
    expect(host.hasAttribute('data-drop-target-rejected')).toBe(false);

    // PDF file drag → rejected
    const pdfDt = fakeDataTransfer({
      types: ['Files'],
      items: [{ kind: 'file', type: 'application/pdf' }],
    });
    const pdfEv = fireDragEvent(host, 'dragover', pdfDt);
    expect(pdfEv.defaultPrevented).toBe(false);
    expect(host.getAttribute('data-drop-target-rejected')).toBe('type');
  });

  it('drop dispatches drop-receive with dataTransfer detail', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const spy = spyEvent(host, 'drop-receive');
    const dt = fakeDataTransfer({ types: ['text/plain'] });

    fireDragEvent(host, 'dragenter', dt);
    fireDragEvent(host, 'dragover', dt);
    const dropEv = fireDragEvent(host, 'drop', dt);
    expect(dropEv.defaultPrevented).toBe(true);
    expect(spy.count).toBe(1);
    expect(spy.last.dataTransfer).toBe(dt);
  });

  it('drop clears data-drop-target-over after firing drop-receive', () => {
    const host = mountHost();
    connectTrait(dropTarget, host);
    const dt = fakeDataTransfer({ types: ['text/plain'] });
    fireDragEvent(host, 'dragenter', dt);
    expect(host.hasAttribute('data-drop-target-over')).toBe(true);
    fireDragEvent(host, 'drop', dt);
    expect(host.hasAttribute('data-drop-target-over')).toBe(false);
  });

  it('drop on rejected payload dispatches drop-rejected, not drop-receive', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': 'image/png' });
    connectTrait(dropTarget, host);
    const receiveSpy = spyEvent(host, 'drop-receive');
    const rejectedSpy = spyEvent(host, 'drop-rejected');
    const dt = fakeDataTransfer({ types: ['text/plain'] });

    fireDragEvent(host, 'drop', dt);
    expect(receiveSpy.count).toBe(0);
    expect(rejectedSpy.count).toBe(1);
    expect(rejectedSpy.last.reason).toBe('type');
  });

  it('respects [disabled] — no event, no over-state', () => {
    const host = mountHost('div', { disabled: '' });
    connectTrait(dropTarget, host);
    const enterSpy = spyEvent(host, 'drop-enter');
    const receiveSpy = spyEvent(host, 'drop-receive');

    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    fireDragEvent(host, 'dragover', fakeDataTransfer());
    fireDragEvent(host, 'drop', fakeDataTransfer());

    expect(host.hasAttribute('data-drop-target-over')).toBe(false);
    expect(enterSpy.count).toBe(0);
    expect(receiveSpy.count).toBe(0);
  });

  it('disconnect removes listeners and clears managed attributes', () => {
    const host = mountHost();
    const inst = connectTrait(dropTarget, host);
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    expect(host.hasAttribute('data-drop-target-over')).toBe(true);
    inst.disconnect(host);
    expect(host.hasAttribute('data-drop-target-over')).toBe(false);
    expect(host.hasAttribute('data-drop-target-active')).toBe(false);
    expect(host.hasAttribute('data-drop-target-rejected')).toBe(false);

    // Re-firing post-disconnect is a no-op.
    const spy = spyEvent(host, 'drop-enter');
    fireDragEvent(host, 'dragenter', fakeDataTransfer());
    expect(spy.count).toBe(0);
  });

  it('clears data-drop-target-rejected when dragover later sees an accepted payload', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': 'image/png' });
    connectTrait(dropTarget, host);

    fireDragEvent(host, 'dragover', fakeDataTransfer({ types: ['text/plain'] }));
    expect(host.getAttribute('data-drop-target-rejected')).toBe('type');

    const pngDt = fakeDataTransfer({
      types: ['Files'],
      items: [{ kind: 'file', type: 'image/png' }],
    });
    fireDragEvent(host, 'dragover', pngDt);
    expect(host.hasAttribute('data-drop-target-rejected')).toBe(false);
  });

  it('dispatches drop-rejected only once per rejection reason on consecutive dragover', () => {
    const host = mountHost('div', { 'data-drop-target-accepts': 'image/png' });
    connectTrait(dropTarget, host);
    const rejectedSpy = spyEvent(host, 'drop-rejected');
    const dt = fakeDataTransfer({ types: ['text/plain'] });

    // Three consecutive dragover events with the same rejection reason
    // should emit ONE drop-rejected, not three.
    fireDragEvent(host, 'dragover', dt);
    fireDragEvent(host, 'dragover', dt);
    fireDragEvent(host, 'dragover', dt);
    expect(rejectedSpy.count).toBe(1);
  });
});
