/**
 * @fileoverview ContextMenu Trigger Handler Test (P4-05)
 * @description
 * Tests for:
 * - Menu item creation and removal
 * - Click event handling
 * - Listener lifecycle
 */

import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { TriggerSpecByKind } from '@/entrypoints/background/record-replay-v3/domain/triggers';
import type { TriggerFireCallback } from '@/entrypoints/background/record-replay-v3/engine/triggers/trigger-handler';
import { createContextMenuTriggerHandlerFactory } from '@/entrypoints/background/record-replay-v3/engine/triggers/context-menu-trigger';

// ==================== Test Utilities ====================

function createSilentLogger(): Pick<Console, 'debug' | 'info' | 'warn' | 'error'> {
  return {
    debug: () => {},
    info: () => {},
    warn: () => {},
    error: () => {},
  };
}

interface ContextMenusMock {
  create: ReturnType<typeof vi.fn>;
  remove: ReturnType<typeof vi.fn>;
  onClicked: {
    addListener: ReturnType<typeof vi.fn>;
    removeListener: ReturnType<typeof vi.fn>;
  };
  emitClicked: (
    info: { menuItemId: string | number; pageUrl?: string },
    tab?: { id?: number; url?: string },
  ) => void;
  createdItems: Map<string, { title: string; contexts: string[] }>;
}

function createContextMenusMock(): ContextMenusMock {
  const listeners = new Set<
    (
      info: { menuItemId: string | number; pageUrl?: string },
      tab?: { id?: number; url?: string },
    ) => void
  >();
  const createdItems = new Map<string, { title: string; contexts: string[] }>();

  const create = vi.fn(
    (props: { id: string; title: string; contexts: string[] }, callback?: () => void) => {
      createdItems.set(props.id, { title: props.title, contexts: props.contexts });
      if (callback) {
        // Simulate async callback
        setTimeout(() => callback(), 0);
      }
      return props.id;
    },
  );

  const remove = vi.fn((menuItemId: string, callback?: () => void) => {
    createdItems.delete(menuItemId);
    if (callback) {
      setTimeout(() => callback(), 0);
    }
  });

  const onClicked = {
    addListener: vi.fn(
      (
        cb: (
          info: { menuItemId: string | number; pageUrl?: string },
          tab?: { id?: number; url?: string },
        ) => void,
      ) => {
        listeners.add(cb);
      },
    ),
    removeListener: vi.fn(
      (
        cb: (
          info: { menuItemId: string | number; pageUrl?: string },
          tab?: { id?: number; url?: string },
        ) => void,
      ) => {
        listeners.delete(cb);
      },
    ),
  };

  return {
    create,
    remove,
    onClicked,
    emitClicked: (info, tab) => {
      for (const cb of listeners) cb(info, tab);
    },
    createdItems,
  };
}

function setupContextMenusMock(): ContextMenusMock {
  const mock = createContextMenusMock();
  (globalThis.chrome as unknown as { contextMenus: unknown }).contextMenus = {
    create: mock.create,
    remove: mock.remove,
    onClicked: mock.onClicked,
  };
  // Clear lastError
  (globalThis.chrome.runtime as { lastError?: { message: string } }).lastError = undefined;
  return mock;
}

// ==================== ContextMenu Trigger Tests ====================

describe('V3 ContextMenuTriggerHandler', () => {
  let contextMenusMock: ContextMenusMock;

  beforeEach(() => {
    contextMenusMock = setupContextMenusMock();
  });

  describe('Menu item creation', () => {
    it('creates menu item on install', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
        contexts: ['page', 'selection'],
      };

      await handler.install(trigger);

      expect(contextMenusMock.create).toHaveBeenCalledWith(
        {
          id: 'rr_v3_t1',
          title: 'Run My Flow',
          contexts: ['page', 'selection'],
        },
        expect.any(Function),
      );
    });

    it('removes stale menu item before creating on install', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);

      expect(contextMenusMock.remove).toHaveBeenCalledWith('rr_v3_t1', expect.any(Function));
      expect(contextMenusMock.remove.mock.invocationCallOrder[0]).toBeLessThan(
        contextMenusMock.create.mock.invocationCallOrder[0],
      );
    });

    it('uses default contexts when not specified', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);

      expect(contextMenusMock.create).toHaveBeenCalledWith(
        expect.objectContaining({
          contexts: ['page'],
        }),
        expect.any(Function),
      );
    });
  });

  describe('Click handling', () => {
    it('fires on menu item click', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);

      contextMenusMock.emitClicked(
        { menuItemId: 'rr_v3_t1', pageUrl: 'https://example.com/page' },
        { id: 123, url: 'https://example.com/page' },
      );

      expect(fireCallback.onFire).toHaveBeenCalledWith('t1', {
        sourceTabId: 123,
        sourceUrl: 'https://example.com/page',
      });
    });

    it('ignores click on non-matching menu item', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);

      contextMenusMock.emitClicked({ menuItemId: 'other_menu_item' });

      expect(fireCallback.onFire).not.toHaveBeenCalled();
    });

    it('uses tab url when pageUrl not available', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);

      contextMenusMock.emitClicked(
        { menuItemId: 'rr_v3_t1' },
        { id: 123, url: 'https://example.com' },
      );

      expect(fireCallback.onFire).toHaveBeenCalledWith('t1', {
        sourceTabId: 123,
        sourceUrl: 'https://example.com',
      });
    });
  });

  describe('Menu item removal', () => {
    it('removes menu item on uninstall', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run My Flow',
      };

      await handler.install(trigger);
      await handler.uninstall('t1');

      expect(contextMenusMock.remove).toHaveBeenCalledWith('rr_v3_t1', expect.any(Function));
    });

    it('removes all menu items on uninstallAll', async () => {
      const fireCallback: TriggerFireCallback = {
        onFire: vi.fn(async () => {}),
      };

      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Flow 1',
      };

      const t2: TriggerSpecByKind<'contextMenu'> = {
        id: 't2' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-2' as never,
        title: 'Flow 2',
      };

      await handler.install(t1);
      await handler.install(t2);
      contextMenusMock.remove.mockClear();
      await handler.uninstallAll();

      expect(contextMenusMock.remove).toHaveBeenCalledTimes(2);
    });
  });

  describe('Listener lifecycle', () => {
    it('registers listener on first install', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Run',
      };

      await handler.install(trigger);

      expect(contextMenusMock.onClicked.addListener).toHaveBeenCalledTimes(1);
    });

    it('removes listener when all triggers uninstalled', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Flow 1',
      };

      const t2: TriggerSpecByKind<'contextMenu'> = {
        id: 't2' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-2' as never,
        title: 'Flow 2',
      };

      await handler.install(t1);
      await handler.install(t2);

      await handler.uninstall('t1');
      expect(contextMenusMock.onClicked.removeListener).not.toHaveBeenCalled();

      await handler.uninstall('t2');
      expect(contextMenusMock.onClicked.removeListener).toHaveBeenCalledTimes(1);
    });
  });

  describe('getInstalledIds', () => {
    it('returns installed trigger IDs', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createContextMenuTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const t1: TriggerSpecByKind<'contextMenu'> = {
        id: 't1' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-1' as never,
        title: 'Flow 1',
      };

      const t2: TriggerSpecByKind<'contextMenu'> = {
        id: 't2' as never,
        kind: 'contextMenu',
        enabled: true,
        flowId: 'flow-2' as never,
        title: 'Flow 2',
      };

      await handler.install(t1);
      await handler.install(t2);

      expect(handler.getInstalledIds().sort()).toEqual(['t1', 't2']);

      await handler.uninstall('t1');
      expect(handler.getInstalledIds()).toEqual(['t2']);
    });
  });
});
