// tests/background/onstartup-notification.test.ts
//
// Plan 01-09 Task 3 RED — pins the onStartup notification + recovery
// notification + notifications.onClicked contract.

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(): 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: {
      query: vi.fn().mockResolvedValue([{ id: 1, url: 'https://example.com', windowId: 100 }]),
      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;
}

describe('Plan 01-09 Task 3: chrome.runtime.onStartup notification contract', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test A — chrome.runtime.onStartup.addListener is registered at SW load
  // ──────────────────────────────────────────────────────────────────────
  it('A: chrome.runtime.onStartup.addListener is registered when SW module loads', 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');

    expect(stub.runtime.onStartup._callbacks.length).toBeGreaterThanOrEqual(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test B — onStartup fires exactly one chrome.notifications.create call
  // with the mokosh-startup- prefix and the expected message shape.
  // ──────────────────────────────────────────────────────────────────────
  it('B: firing onStartup creates exactly one mokosh-startup- notification', 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');

    const createCallsBefore = stub.notifications.create.mock.calls.length;

    // Fire onStartup.
    const startupCb = stub.runtime.onStartup._callbacks[0];
    startupCb();
    for (let i = 0; i < 8; i++) await Promise.resolve();

    const startupCalls = stub.notifications.create.mock.calls
      .slice(createCallsBefore)
      .filter((args: unknown[]) => {
        const id = args[0] as unknown;
        return typeof id === 'string' && id.startsWith('mokosh-startup-');
      });
    expect(startupCalls.length).toBe(1);

    // Validate the options shape.
    const opts = startupCalls[0][1] as {
      type?: unknown; title?: unknown; message?: unknown;
    };
    expect(opts.type).toBe('basic');
    // Plan 01-12 Wave 4: notification title migrated from literal
    // 'Mokosh ready' to chrome.i18n.getMessage('extName') with fallback
    // 'Mokosh' (NOTIF_EXTNAME_FALLBACK). Tests without a chrome.i18n
    // stub observe the fallback. Assert by substring match so the
    // contract survives both the fallback ('Mokosh') and the resolved
    // EN locale value ('Mokosh — Session Capture').
    expect(typeof opts.title).toBe('string');
    expect(/Mokosh/i.test(String(opts.title))).toBe(true);
    expect(typeof opts.message).toBe('string');
    // Plan 01-12 Wave 4: notification message migrated to chrome.i18n.
    // Plan 01-09 amendment 2026-05-20: key split — onStartup handler now
    // reads `notifStartupCta` with fallback `NOTIF_STARTUP_CTA_FALLBACK`
    // ('Mokosh ready. Click to start a recording.'); the prior conflated
    // `notifStartup` key was retired (`notifRecordingStarted` reserved
    // for post-manual-start confirmation). Tests without a chrome.i18n
    // stub observe the CTA fallback; the regex matches both 'click' and
    // 'recording' (case-insensitive) so it survives both the fallback
    // and the resolved locale value.
    expect(/recording|recor|click/i.test(String(opts.message))).toBe(true);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test C — chrome.notifications.onClicked with a mokosh-startup- id
  // calls clear() AND triggers startVideoCapture (sendMessage START_RECORDING).
  // ──────────────────────────────────────────────────────────────────────
  it('C: notifications.onClicked with mokosh- id clears + triggers START_RECORDING', 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');

    // Set up port + OFFSCREEN_READY so startVideoCapture proceeds.
    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 notification click.
    const notifCb = stub.notifications.onClicked._callbacks[0];
    notifCb('mokosh-startup-1234567890');
    for (let i = 0; i < 32; i++) await Promise.resolve();

    // clear() was called with the same id.
    const clearCalls = stub.notifications.clear.mock.calls.filter(
      (args: unknown[]) => args[0] === 'mokosh-startup-1234567890',
    );
    expect(clearCalls.length).toBeGreaterThanOrEqual(1);

    // START_RECORDING sent downstream.
    const startCalls = 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';
      },
    );
    expect(startCalls.length).toBeGreaterThanOrEqual(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test D — receiving RECORDING_ERROR via onMessage triggers a recovery
  // notification (id prefix 'mokosh-recovery-').
  // ──────────────────────────────────────────────────────────────────────
  it('D: RECORDING_ERROR onMessage triggers a mokosh-recovery- notification', 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');

    const createCallsBefore = stub.notifications.create.mock.calls.length;

    const onMsgHandler = stub.runtime.onMessage._callbacks[0];
    onMsgHandler(
      { type: 'RECORDING_ERROR', error: 'capture-failed' },
      { id: 'ext-id-test' },
      vi.fn(),
    );
    for (let i = 0; i < 16; i++) await Promise.resolve();

    const recoveryCalls = stub.notifications.create.mock.calls
      .slice(createCallsBefore)
      .filter((args: unknown[]) => {
        const id = args[0] as unknown;
        return typeof id === 'string' && id.startsWith('mokosh-recovery-');
      });
    expect(recoveryCalls.length).toBeGreaterThanOrEqual(1);
  });
});
