// tests/background/onboarding.test.ts
//
// Plan 01-10 Task 1 — RED tests pinning the onInstalled welcome-tab
// contract per Plan 01-10 D-17-onboarding.
//
// Three tests synthesize chrome.runtime.onInstalled events with the
// reasons {install,update} crossed with the storage flag state {empty,
// already-set}. The production helper `openWelcomeIfFirstInstall` does
// not yet exist (Task 3 lands it); these tests MUST be RED until Task 3
// flips them GREEN.
//
// Stub scaffold: copied from `tests/background/onstartup-notification.test.ts`
// + extended with chrome.tabs.create, chrome.storage.local.{get,set}, and
// chrome.runtime.onInstalled.{addListener,_callbacks}.

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 OnInstalledCallback {
  (details: chrome.runtime.InstalledDetails): void | Promise<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>;
      _callbacks: OnInstalledCallback[];
    };
    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>;
    create: ReturnType<typeof vi.fn>;
  };
  storage: {
    local: {
      get: ReturnType<typeof vi.fn>;
      set: 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> };
}

/**
 * Build a fresh chrome.* stub object for one test. The onInstalled
 * `_callbacks` array is the one Plan 01-10's openWelcomeIfFirstInstall
 * fixture relies on: the addListener mockImplementation pushes any
 * listener registered by `src/background/index.ts` so the test can
 * synthesize the install event by invoking _callbacks[0] with the
 * details object.
 */
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(), _callbacks: [] },
      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'),
      create: vi.fn().mockResolvedValue({ id: 99, url: 'x' }),
    },
    storage: {
      local: {
        get: vi.fn(),
        set: vi.fn().mockResolvedValue(undefined),
      },
    },
    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);
  });
  stub.runtime.onInstalled.addListener.mockImplementation((cb: OnInstalledCallback) => {
    stub.runtime.onInstalled._callbacks.push(cb);
  });
  return stub;
}

/**
 * Drain the microtask queue so any `.then` chains in the SW's
 * fire-and-forget onInstalled flow have a chance to complete. The
 * SW handler is synchronous; openWelcomeIfFirstInstall is awaited
 * inside the helper but kicked off as fire-and-forget from the
 * listener. We pump 16 microtasks (generous; the helper has at most
 * three awaits — storage.get, tabs.create, storage.set).
 */
async function drainMicrotasks(): Promise<void> {
  for (let i = 0; i < 16; i += 1) {
    await Promise.resolve();
  }
}

describe('Plan 01-10 onboarding contract', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test A — first install + empty storage opens welcome tab + sets flag.
  // ──────────────────────────────────────────────────────────────────────
  it('A: first install + empty storage opens exactly one welcome tab and sets the storage flag', async () => {
    const stub = buildBgStub();
    stub.storage.local.get.mockResolvedValue({});
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = {
      deleteDatabase: vi.fn(),
    };

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

    expect(stub.runtime.onInstalled._callbacks.length).toBeGreaterThanOrEqual(1);
    const installedCb = stub.runtime.onInstalled._callbacks[0];
    expect(installedCb).toBeTypeOf('function');

    installedCb({ reason: 'install' } as chrome.runtime.InstalledDetails);
    await drainMicrotasks();

    // 1. chrome.tabs.create called exactly once with a URL pointing at
    //    src/welcome/welcome.html.
    expect(stub.tabs.create).toHaveBeenCalledTimes(1);
    const createArgs = stub.tabs.create.mock.calls[0][0] as { url?: unknown };
    expect(typeof createArgs.url).toBe('string');
    expect(String(createArgs.url)).toContain('src/welcome/welcome.html');

    // 2. chrome.storage.local.set called with the onboarding flag + installed-at.
    expect(stub.storage.local.set).toHaveBeenCalledTimes(1);
    const setArg = stub.storage.local.set.mock.calls[0][0] as Record<string, unknown>;
    expect(setArg['onboarding-completed']).toBe(true);
    expect(typeof setArg['installed-at']).toBe('number');

    // 3. chrome.storage.local.get called with the EXACT key string
    //    'onboarding-completed' (storage-schema cross-version-compat pin;
    //    preserves the I-02 lesson from the prior draft).
    expect(stub.storage.local.get).toHaveBeenCalled();
    const getCalls = stub.storage.local.get.mock.calls;
    const sawExactKey = getCalls.some((args: unknown[]) => {
      const key = args[0];
      return key === 'onboarding-completed';
    });
    expect(sawExactKey).toBe(true);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test B — subsequent install (reason='update') opens NO welcome tab.
  // ──────────────────────────────────────────────────────────────────────
  it('B: subsequent install (reason=update) does NOT open a welcome tab', async () => {
    const stub = buildBgStub();
    stub.storage.local.get.mockResolvedValue({});
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = {
      deleteDatabase: vi.fn(),
    };

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

    const installedCb = stub.runtime.onInstalled._callbacks[0];
    expect(installedCb).toBeTypeOf('function');

    installedCb({ reason: 'update', previousVersion: '0.9.9' } as chrome.runtime.InstalledDetails);
    await drainMicrotasks();

    expect(stub.tabs.create).not.toHaveBeenCalled();
    expect(stub.storage.local.set).not.toHaveBeenCalled();
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test C — install + flag already set opens NO welcome tab.
  // ──────────────────────────────────────────────────────────────────────
  it('C: install but onboarding-completed already true does NOT open a welcome tab', async () => {
    const stub = buildBgStub();
    stub.storage.local.get.mockResolvedValue({
      'onboarding-completed': true,
    });
    (globalThis as unknown as GlobalWithBgChrome).chrome = stub;
    (globalThis as unknown as GlobalWithBgChrome).indexedDB = {
      deleteDatabase: vi.fn(),
    };

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

    const installedCb = stub.runtime.onInstalled._callbacks[0];
    expect(installedCb).toBeTypeOf('function');

    installedCb({ reason: 'install' } as chrome.runtime.InstalledDetails);
    await drainMicrotasks();

    expect(stub.tabs.create).not.toHaveBeenCalled();
    // storage.set MAY be called by other init paths; we only assert the
    // welcome-tab side-effect did NOT fire. The storage-set check here
    // is intentionally weak — Test A pins the positive contract.
  });
});
