import { describe, expect, it } from 'vitest';
import type { BrowserContext } from 'playwright';
import { applyAgentSnapshotHooks } from './browser-launch.js';

/**
 * Capture the init-script function passed to BrowserContext.addInitScript so
 * we can run it in a controlled Node sandbox. Playwright would normally ship
 * the function string to the browser, but in tests we exercise it directly
 * against a synthetic Element/EventTarget pair.
 */
function makeFakeContext(): { context: BrowserContext; scripts: Array<() => void> } {
  const scripts: Array<() => void> = [];
  const context = {
    addInitScript: (fn: () => void) => {
      scripts.push(fn);
      return Promise.resolve();
    },
  } as unknown as BrowserContext;
  return { context, scripts };
}

describe('applyAgentSnapshotHooks', () => {
  it('passes a function to addInitScript', async () => {
    const { context, scripts } = makeFakeContext();
    await applyAgentSnapshotHooks(context);
    expect(scripts.length).toBe(1);
    expect(typeof scripts[0]).toBe('function');
  });

  it('marks elements receiving click/mousedown/pointerdown listeners with data-ao-click', async () => {
    const { context, scripts } = makeFakeContext();
    await applyAgentSnapshotHooks(context);

    class FakeElement extends EventTarget {
      private readonly attrs = new Map<string, string>();
      setAttribute(k: string, v: string): void { this.attrs.set(k, v); }
      hasAttribute(k: string): boolean { return this.attrs.has(k); }
      getAttribute(k: string): string | null { return this.attrs.get(k) ?? null; }
    }

    const originalElement = (globalThis as { Element?: unknown }).Element;
    const originalAdd = EventTarget.prototype.addEventListener;
    (globalThis as { Element?: unknown }).Element = FakeElement;

    try {
      scripts[0]!();

      const click = new FakeElement();
      click.addEventListener('click', () => { /* noop */ });
      expect(click.hasAttribute('data-ao-click')).toBe(true);

      const mouseDown = new FakeElement();
      mouseDown.addEventListener('mousedown', () => { /* noop */ });
      expect(mouseDown.hasAttribute('data-ao-click')).toBe(true);

      const pointerDown = new FakeElement();
      pointerDown.addEventListener('pointerdown', () => { /* noop */ });
      expect(pointerDown.hasAttribute('data-ao-click')).toBe(true);
    } finally {
      EventTarget.prototype.addEventListener = originalAdd;
      if (originalElement === undefined) {
        delete (globalThis as { Element?: unknown }).Element;
      } else {
        (globalThis as { Element?: unknown }).Element = originalElement;
      }
    }
  });

  it('does not mark elements for unrelated event types', async () => {
    const { context, scripts } = makeFakeContext();
    await applyAgentSnapshotHooks(context);

    class FakeElement extends EventTarget {
      private readonly attrs = new Map<string, string>();
      setAttribute(k: string, v: string): void { this.attrs.set(k, v); }
      hasAttribute(k: string): boolean { return this.attrs.has(k); }
    }

    const originalElement = (globalThis as { Element?: unknown }).Element;
    const originalAdd = EventTarget.prototype.addEventListener;
    (globalThis as { Element?: unknown }).Element = FakeElement;

    try {
      scripts[0]!();

      const el = new FakeElement();
      el.addEventListener('mouseover', () => { /* noop */ });
      el.addEventListener('keydown', () => { /* noop */ });
      el.addEventListener('focus', () => { /* noop */ });
      expect(el.hasAttribute('data-ao-click')).toBe(false);
    } finally {
      EventTarget.prototype.addEventListener = originalAdd;
      if (originalElement === undefined) {
        delete (globalThis as { Element?: unknown }).Element;
      } else {
        (globalThis as { Element?: unknown }).Element = originalElement;
      }
    }
  });

  it('does not mark non-Element EventTargets', async () => {
    const { context, scripts } = makeFakeContext();
    await applyAgentSnapshotHooks(context);

    class FakeElement extends EventTarget {
      private readonly attrs = new Map<string, string>();
      setAttribute(k: string, v: string): void { this.attrs.set(k, v); }
      hasAttribute(k: string): boolean { return this.attrs.has(k); }
    }

    const originalElement = (globalThis as { Element?: unknown }).Element;
    const originalAdd = EventTarget.prototype.addEventListener;
    (globalThis as { Element?: unknown }).Element = FakeElement;

    try {
      scripts[0]!();

      // Plain EventTarget (not a FakeElement) — must not be tagged because
      // `this instanceof Element` should be false.
      const target = new EventTarget();
      let setAttrCalled = false;
      Object.defineProperty(target, 'setAttribute', {
        value: () => { setAttrCalled = true; },
      });
      target.addEventListener('click', () => { /* noop */ });
      expect(setAttrCalled).toBe(false);
    } finally {
      EventTarget.prototype.addEventListener = originalAdd;
      if (originalElement === undefined) {
        delete (globalThis as { Element?: unknown }).Element;
      } else {
        (globalThis as { Element?: unknown }).Element = originalElement;
      }
    }
  });

  it('still forwards the call to the original addEventListener', async () => {
    const { context, scripts } = makeFakeContext();
    await applyAgentSnapshotHooks(context);

    class FakeElement extends EventTarget {
      setAttribute(_k: string, _v: string): void { /* noop */ }
    }

    const originalElement = (globalThis as { Element?: unknown }).Element;
    const originalAdd = EventTarget.prototype.addEventListener;
    (globalThis as { Element?: unknown }).Element = FakeElement;

    try {
      scripts[0]!();

      const el = new FakeElement();
      let fired = 0;
      el.addEventListener('click', () => { fired++; });
      el.dispatchEvent(new Event('click'));
      expect(fired).toBe(1);
    } finally {
      EventTarget.prototype.addEventListener = originalAdd;
      if (originalElement === undefined) {
        delete (globalThis as { Element?: unknown }).Element;
      } else {
        (globalThis as { Element?: unknown }).Element = originalElement;
      }
    }
  });
});
