// tests/background/start-video-capture-no-tab.test.ts
//
// Plan 01-09 — Debug `01-09-notification-start-no-active-tab`
//
// Regression pin: startVideoCapture must NOT depend on the presence of
// an active tab. Post Plan 01-09 D-01 (chrome.tabCapture → getDisplayMedia
// in offscreen) the SW-side recording-start path has no functional need
// for a tab — the only consumer of the legacy tab query was a log line.
//
// chrome.notifications.onClicked does NOT grant the activeTab permission
// and the extension manifest carries no `tabs` permission (per
// .planning/intel/01-11-SUMMARY.md follow-up backlog). So in production,
// chrome.tabs.query({ active: true, currentWindow: true }) called from
// inside the notifications.onClicked path resolves to `[]` (or a tab
// stripped of `url` / `id`). The previous implementation rejected with
// "No active tab found" before reaching the ensureOffscreen + START_RECORDING
// dispatch — the operator-observed silent failure of the onStartup
// notification CTA path.
//
// These tests pin both halves of the contract:
//   * empty tabs.query result → still dispatches START_RECORDING (no throw)
//   * url-less tab → still dispatches START_RECORDING (no throw)
//
// They are isomorphic to the action.onClicked path which today works
// because activeTab is granted on the click gesture. Removing the dead
// tab query equalises the two callers.

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

interface OnConnectCallback { (port: unknown): void; }
interface OnMessageCallback {
  (msg: unknown, sender: { id?: string }, sendResponse: (r: unknown) => void): boolean;
}
interface OnClickedCallback { (info?: unknown): void | Promise<void>; }
interface OnNotificationClickedCallback { (notificationId: string): void; }

interface BgChromeStub {
  runtime: {
    id: string;
    getURL: (p: string) => string;
    getManifest: () => { version: string };
    sendMessage: ReturnType<typeof vi.fn>;
    onMessage: { addListener: ReturnType<typeof vi.fn>; _callbacks: OnMessageCallback[] };
    onConnect: { addListener: (cb: OnConnectCallback) => void; _callbacks: OnConnectCallback[] };
    onInstalled: { addListener: ReturnType<typeof vi.fn> };
    onStartup: { addListener: (cb: () => void) => void; _callbacks: Array<() => void> };
  };
  offscreen: {
    hasDocument?: () => Promise<boolean>;
    createDocument: ReturnType<typeof vi.fn>;
    Reason: { DISPLAY_MEDIA: 'DISPLAY_MEDIA' };
  };
  tabs: {
    query: ReturnType<typeof vi.fn>;
    sendMessage: ReturnType<typeof vi.fn>;
    captureVisibleTab: ReturnType<typeof vi.fn>;
  };
  downloads: { download: ReturnType<typeof vi.fn> };
  action: {
    onClicked: { addListener: (cb: OnClickedCallback) => void; _callbacks: OnClickedCallback[] };
    setPopup: ReturnType<typeof vi.fn>;
    setBadgeText: ReturnType<typeof vi.fn>;
    setBadgeBackgroundColor: ReturnType<typeof vi.fn>;
    setTitle: ReturnType<typeof vi.fn>;
  };
  notifications: {
    create: ReturnType<typeof vi.fn>;
    clear: ReturnType<typeof vi.fn>;
    onClicked: {
      addListener: (cb: OnNotificationClickedCallback) => void;
      _callbacks: OnNotificationClickedCallback[];
    };
  };
}

interface GlobalWithBgChrome {
  chrome?: BgChromeStub;
  indexedDB?: { deleteDatabase: ReturnType<typeof vi.fn> };
}

function buildBgStub(tabsQueryResult: unknown[]): BgChromeStub {
  const stub: BgChromeStub = {
    runtime: {
      id: 'ext-id-test',
      getURL: (p) => `chrome-extension://ext-id-test/${p}`,
      getManifest: () => ({ version: '1.0.0' }),
      sendMessage: vi.fn().mockResolvedValue(undefined),
      onMessage: { addListener: vi.fn(), _callbacks: [] },
      onConnect: { addListener: (cb) => stub.runtime.onConnect._callbacks.push(cb), _callbacks: [] },
      onInstalled: { addListener: vi.fn() },
      onStartup: { addListener: (cb) => stub.runtime.onStartup._callbacks.push(cb), _callbacks: [] },
    },
    offscreen: {
      hasDocument: async () => false,
      createDocument: vi.fn().mockResolvedValue(undefined),
      Reason: { DISPLAY_MEDIA: 'DISPLAY_MEDIA' },
    },
    tabs: {
      // Mirror the production notifications.onClicked context: no activeTab
      // grant + no `tabs` permission → tabs.query returns `[]`. The
      // url-less-tab variant is supplied via the parameter.
      query: vi.fn().mockResolvedValue(tabsQueryResult),
      sendMessage: vi.fn().mockResolvedValue({ events: [], userEvents: [] }),
      captureVisibleTab: vi.fn().mockResolvedValue('data:image/png;base64,xx'),
    },
    downloads: { download: vi.fn().mockResolvedValue(1) },
    action: {
      onClicked: { addListener: (cb) => stub.action.onClicked._callbacks.push(cb), _callbacks: [] },
      setPopup: vi.fn(),
      setBadgeText: vi.fn(),
      setBadgeBackgroundColor: vi.fn(),
      setTitle: vi.fn(),
    },
    notifications: {
      create: vi.fn(),
      clear: vi.fn(),
      onClicked: {
        addListener: (cb) => stub.notifications.onClicked._callbacks.push(cb),
        _callbacks: [],
      },
    },
  };
  stub.runtime.onMessage.addListener.mockImplementation((cb: OnMessageCallback) => {
    stub.runtime.onMessage._callbacks.push(cb);
  });
  return stub;
}

/**
 * Drive the SW startVideoCapture path via notifications.onClicked + a
 * connected video-keepalive port + an OFFSCREEN_READY onMessage event,
 * which is the same await-offscreenReady satisfaction that the existing
 * onstartup-notification.test.ts Test C uses.
 */
async function driveNotificationStart(stub: BgChromeStub): Promise<void> {
  const port = {
    name: 'video-keepalive',
    sender: { id: 'ext-id-test' },
    postMessage: vi.fn(),
    onMessage: { addListener: vi.fn() },
    onDisconnect: { addListener: vi.fn() },
    disconnect: vi.fn(),
  };
  if (stub.runtime.onConnect._callbacks.length > 0) {
    stub.runtime.onConnect._callbacks[0](port);
  }
  const onMsgHandler = stub.runtime.onMessage._callbacks[0];
  onMsgHandler({ type: 'OFFSCREEN_READY' }, { id: 'ext-id-test' }, vi.fn());

  // Synthesize the click on a mokosh-* notification id.
  const notifCb = stub.notifications.onClicked._callbacks[0];
  notifCb('mokosh-startup-1234567890');
  // Drain microtasks so the entire async chain reaches sendMessage.
  for (let i = 0; i < 64; i++) await Promise.resolve();
}

function countStartRecording(stub: BgChromeStub): number {
  return stub.runtime.sendMessage.mock.calls.filter((args: unknown[]) => {
    const msg = args[0] as { type?: unknown };
    return typeof msg === 'object' && msg !== null && msg.type === 'START_RECORDING';
  }).length;
}

describe('Plan 01-09 — startVideoCapture: no active-tab dependency', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test A — RED today, GREEN after fix:
  // chrome.tabs.query returns [] (real production notifications.onClicked
  // context, since the extension has no `tabs` permission and notification
  // clicks do not grant activeTab) → startVideoCapture must NOT throw
  // "No active tab found"; START_RECORDING must still be dispatched.
  // ──────────────────────────────────────────────────────────────────────
  it('A: tabs.query → [] still dispatches START_RECORDING (no "No active tab" throw)', async () => {
    const stub = buildBgStub([]);
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = { deleteDatabase: vi.fn() };

    await import('../../src/background/index');

    await driveNotificationStart(stub);

    expect(countStartRecording(stub)).toBeGreaterThanOrEqual(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test B — RED today, GREEN after fix:
  // tabs.query returns a tab whose `url` is undefined (the activeTab-less
  // shape Chrome surfaces when the caller lacks both activeTab and `tabs`
  // permission) → startVideoCapture must NOT throw; START_RECORDING is
  // dispatched.
  // ──────────────────────────────────────────────────────────────────────
  it('B: tabs.query → [{id, url: undefined}] still dispatches START_RECORDING', async () => {
    const stub = buildBgStub([{ id: 1 }]);
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = { deleteDatabase: vi.fn() };

    await import('../../src/background/index');

    await driveNotificationStart(stub);

    expect(countStartRecording(stub)).toBeGreaterThanOrEqual(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test C — REGRESSION GUARD (was GREEN today, MUST stay GREEN after fix):
  // tabs.query returns a fully-populated tab (the action.onClicked /
  // click-gesture-activeTab context) → still dispatches START_RECORDING.
  // Pins that the fix does not over-trim and break the toolbar path.
  // ──────────────────────────────────────────────────────────────────────
  it('C: tabs.query → [{id, url}] still dispatches START_RECORDING (regression guard)', async () => {
    const stub = buildBgStub([{ id: 1, url: 'https://example.com', windowId: 100 }]);
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = { deleteDatabase: vi.fn() };

    await import('../../src/background/index');

    await driveNotificationStart(stub);

    expect(countStartRecording(stub)).toBeGreaterThanOrEqual(1);
  });
});
