// tests/background/toolbar-action.test.ts
//
// Plan 01-09 Task 3 RED — pins the chrome.action.onClicked routing
// contract + setPopup state-machine + popup SAVE-only idle-race
// behavior (W-02 fix). 5 tests; A/B/C/D RED for background, E RED
// for popup-init. Task 4 GREEN.

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

interface PortStub {
  name: string;
  sender?: { id?: string };
  postMessage: ReturnType<typeof vi.fn>;
  onMessage: { addListener: ReturnType<typeof vi.fn>; _listeners: Array<(m: unknown) => void> };
  onDisconnect: { addListener: (fn: () => void) => void; _listeners: Array<() => void> };
  disconnect: ReturnType<typeof vi.fn>;
}

function makePortStub(name = 'video-keepalive'): PortStub {
  const port: PortStub = {
    name,
    sender: { id: 'ext-id-test' },
    postMessage: vi.fn(),
    onMessage: { addListener: vi.fn(), _listeners: [] },
    onDisconnect: {
      addListener: (fn) => { port.onDisconnect._listeners.push(fn); },
      _listeners: [],
    },
    disconnect: vi.fn(),
  };
  port.onMessage.addListener.mockImplementation((fn: (m: unknown) => void) => {
    port.onMessage._listeners.push(fn);
  });
  return port;
}

interface OnConnectCallback { (port: PortStub): 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> };
  fetch?: typeof fetch;
}

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.action.onClicked routing + setPopup state-machine', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test A — chrome.action.onClicked.addListener is registered at SW init
  // ──────────────────────────────────────────────────────────────────────
  it('A: chrome.action.onClicked.addListener is registered when SW initializes', 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.action.onClicked._callbacks.length).toBeGreaterThanOrEqual(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test B — onClicked while NOT recording invokes startVideoCapture
  // ──────────────────────────────────────────────────────────────────────
  it('B: onClicked while not recording triggers startVideoCapture (sends 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');

    // Synthetically connect an offscreen port + signal OFFSCREEN_READY so
    // startVideoCapture's await offscreenReady resolves.
    const port = makePortStub();
    if (stub.runtime.onConnect._callbacks.length > 0) {
      stub.runtime.onConnect._callbacks[0](port);
    }
    // Fire OFFSCREEN_READY via the onMessage callback so the readiness
    // Promise resolves.
    const onMsgHandler = stub.runtime.onMessage._callbacks[0];
    onMsgHandler({ type: 'OFFSCREEN_READY' }, { id: 'ext-id-test' }, vi.fn());

    const cb = stub.action.onClicked._callbacks[0];
    cb();

    // Drain microtasks so startVideoCapture's await chain reaches sendMessage.
    for (let i = 0; i < 32; i++) await Promise.resolve();

    // The handler should have triggered a START_RECORDING sendMessage.
    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 C — onClicked twice without intermediate state-clear does not
  // double-start (defensive guard test). After the first onClicked fires
  // and isRecording flips true, a second onClicked must NOT trigger a
  // second startVideoCapture.
  // ──────────────────────────────────────────────────────────────────────
  it('C: onClicked while isRecording is true does NOT double-start', 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 port = makePortStub();
    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());

    const cb = stub.action.onClicked._callbacks[0];
    // First click — should fire START_RECORDING and flip isRecording.
    cb();
    for (let i = 0; i < 32; i++) await Promise.resolve();
    // Second click while already recording — should be a no-op.
    cb();
    for (let i = 0; i < 32; i++) await Promise.resolve();

    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';
      },
    );
    // Exactly ONE START_RECORDING across both clicks.
    expect(startCalls.length).toBe(1);
  });

  // ──────────────────────────────────────────────────────────────────────
  // Test D — setPopup state-machine: empty string in OFF (idle) mode, the
  // popup html path in REC mode. Verified via setPopup spy call args.
  // ──────────────────────────────────────────────────────────────────────
  it('D: setPopup is called with "" in OFF mode and with popup html path in REC mode', 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');

    // After init the SW should have entered idle mode → setPopup('').
    for (let i = 0; i < 8; i++) await Promise.resolve();
    const offCalls = stub.action.setPopup.mock.calls.filter(
      (args: unknown[]) => {
        const opts = args[0] as { popup?: unknown };
        return typeof opts === 'object' && opts !== null && opts.popup === '';
      },
    );
    expect(offCalls.length).toBeGreaterThanOrEqual(1);

    // Now trigger startVideoCapture via onClicked; this should set the
    // popup back to the html path (REC mode).
    const port = makePortStub();
    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());

    const cb = stub.action.onClicked._callbacks[0];
    cb();
    for (let i = 0; i < 32; i++) await Promise.resolve();

    const recCalls = stub.action.setPopup.mock.calls.filter(
      (args: unknown[]) => {
        const opts = args[0] as { popup?: unknown };
        return (
          typeof opts === 'object' &&
          opts !== null &&
          typeof opts.popup === 'string' &&
          (opts.popup as string).includes('popup/index.html')
        );
      },
    );
    expect(recCalls.length).toBeGreaterThanOrEqual(1);
  });
});

// ────────────────────────────────────────────────────────────────────────
// Test E — popup-loads-when-idle race pin (W-02 fix).
//
// Node-env-friendly: jsdom is not installed; instead we manually stub
// the document/event surface the popup module touches at init time.
// The popup uses: getElementById('saveButton'/'statusMessage'),
// querySelector('.button-text'), addEventListener (button + document).
// ────────────────────────────────────────────────────────────────────────

interface ElementStub {
  textContent: string;
  className: string;
  disabled?: boolean;
  querySelector?: (sel: string) => ElementStub | null;
  addEventListener?: (ev: string, cb: () => void) => void;
}

interface DocumentStub {
  getElementById: (id: string) => ElementStub | null;
  addEventListener: (ev: string, cb: () => void) => void;
  _docListeners: Map<string, Array<() => void>>;
  querySelector: (sel: string) => ElementStub | null;
  querySelectorAll: (sel: string) => ElementStub[];
}

function buildDocumentStub(): {
  doc: DocumentStub;
  saveBtn: ElementStub;
  statusMsg: ElementStub;
  buttonText: ElementStub;
} {
  const buttonText: ElementStub = {
    textContent: '',
    className: '',
  };
  const saveBtn: ElementStub = {
    textContent: '',
    className: '',
    disabled: true,
    querySelector: (sel: string) => (sel === '.button-text' ? buttonText : null),
    addEventListener: vi.fn(),
  };
  const statusMsg: ElementStub = {
    textContent: '',
    className: '',
  };
  const docListeners = new Map<string, Array<() => void>>();
  const doc: DocumentStub = {
    getElementById: (id: string) => {
      if (id === 'saveButton') return saveBtn;
      if (id === 'statusMessage') return statusMsg;
      return null;
    },
    addEventListener: (ev: string, cb: () => void) => {
      const list = docListeners.get(ev) ?? [];
      list.push(cb);
      docListeners.set(ev, list);
    },
    _docListeners: docListeners,
    querySelector: () => null,
    // Plan 01-12 Wave 4: populateMksKeys() in src/popup/index.ts uses
    // querySelectorAll('[data-mks-key]') to populate i18n copy on init.
    // The toolbar-action test loads the popup without a real DOM, so
    // returning an empty array safely is the right answer (no data-mks-key
    // elements exist in the stubbed doc; populate-loop is a no-op).
    querySelectorAll: () => [],
  };
  return { doc, saveBtn, statusMsg, buttonText };
}

describe('Plan 01-09 Task 3 Test E: popup SAVE-only contract (W-02 fix)', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  it('E1+E2: popup init does NOT send REQUEST_PERMISSIONS AND saveButton is enabled', async () => {
    const { doc, saveBtn } = buildDocumentStub();
    (globalThis as unknown as { document?: unknown }).document = doc;

    const sendMessageSpy = vi.fn().mockResolvedValue({ granted: false });
    (globalThis as unknown as { chrome?: unknown }).chrome = {
      runtime: {
        id: 'ext-id-test',
        sendMessage: sendMessageSpy,
        onMessage: { addListener: vi.fn() },
      },
    };

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

    // Fire DOMContentLoaded via the captured doc listeners.
    const cbs = doc._docListeners.get('DOMContentLoaded') ?? [];
    cbs.forEach((cb) => cb());

    // Drain microtasks so any async init paths complete.
    for (let i = 0; i < 16; i++) await Promise.resolve();

    // E1 — no REQUEST_PERMISSIONS at popup-open.
    const reqPermCalls = sendMessageSpy.mock.calls.filter(
      (args: unknown[]) => {
        const msg = args[0] as { type?: unknown };
        return typeof msg === 'object' && msg !== null && msg.type === 'REQUEST_PERMISSIONS';
      },
    );
    expect(reqPermCalls.length).toBe(0);

    // E2 — SAVE button enabled regardless of chrome round-trip result.
    expect(saveBtn.disabled).toBe(false);
  });
});
