import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  isExtensionValid,
  safeStorageGet,
  safeStorageSet,
  safeSendMessage,
} from '@lib/messaging';

// Rebuild the chrome mock per test so each case controls runtime.id presence.
function mockChrome({ valid = true }: { valid?: boolean } = {}) {
  const local = {
    get: vi.fn((_keys: string[], cb: (r: Record<string, unknown>) => void) => cb({})),
    set: vi.fn((_data: Record<string, unknown>, cb?: () => void) => cb && cb()),
  };
  const session = {
    get: vi.fn((_keys: string[], cb: (r: Record<string, unknown>) => void) => cb({})),
    set: vi.fn((_data: Record<string, unknown>, cb?: () => void) => cb && cb()),
  };
  global.chrome = {
    storage: { local, session },
    runtime: {
      id: valid ? 'test-extension-id' : undefined,
      sendMessage: vi.fn().mockResolvedValue({ result: 'ok' }),
    },
  } as any;
  return { local, session };
}

beforeEach(() => {
  vi.restoreAllMocks();
});

describe('isExtensionValid', () => {
  it('is true when chrome.runtime.id exists', () => {
    mockChrome({ valid: true });
    expect(isExtensionValid()).toBe(true);
  });

  it('is false when the extension context is invalidated (no runtime.id)', () => {
    mockChrome({ valid: false });
    expect(isExtensionValid()).toBe(false);
  });
});

describe('safeStorageGet', () => {
  it('reads from chrome.storage.local by default', () => {
    const { local } = mockChrome();
    const cb = vi.fn();
    safeStorageGet(['provider', 'model'], cb);
    expect(local.get).toHaveBeenCalledWith(['provider', 'model'], cb);
    expect(cb).toHaveBeenCalledWith({});
  });

  it('reads from an explicitly passed storage area (e.g. session)', () => {
    const { local, session } = mockChrome();
    const cb = vi.fn();
    safeStorageGet(['lexai_popup_input'], cb, (global.chrome as any).storage.session);
    expect(session.get).toHaveBeenCalled();
    expect(local.get).not.toHaveBeenCalled();
  });

  it('is a silent no-op when the extension context is gone', () => {
    const { local } = mockChrome({ valid: false });
    const cb = vi.fn();
    safeStorageGet(['provider'], cb);
    expect(local.get).not.toHaveBeenCalled();
    expect(cb).not.toHaveBeenCalled();
  });

  it('swallows a throwing storage call with a warning', () => {
    mockChrome();
    (global.chrome as any).storage.local.get = vi.fn(() => {
      throw new Error('Extension context invalidated.');
    });
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    expect(() => safeStorageGet(['provider'], vi.fn())).not.toThrow();
    expect(warn).toHaveBeenCalled();
  });
});

describe('safeStorageSet', () => {
  it('writes to chrome.storage.local by default and honors the callback', () => {
    const { local } = mockChrome();
    const done = vi.fn();
    safeStorageSet({ provider: 'openai' }, done);
    expect(local.set).toHaveBeenCalledWith({ provider: 'openai' }, done);
    expect(done).toHaveBeenCalled();
  });

  it('writes without a callback', () => {
    const { local } = mockChrome();
    safeStorageSet({ writingStyle: 'Formal' });
    expect(local.set).toHaveBeenCalledWith({ writingStyle: 'Formal' });
  });

  it('writes to an explicitly passed storage area', () => {
    const { session } = mockChrome();
    safeStorageSet({ lexai_popup_input: 'draft' }, undefined, (global.chrome as any).storage.session);
    expect(session.set).toHaveBeenCalledWith({ lexai_popup_input: 'draft' });
  });

  it('is a no-op when the extension context is gone', () => {
    const { local } = mockChrome({ valid: false });
    safeStorageSet({ provider: 'openai' });
    expect(local.set).not.toHaveBeenCalled();
  });
});

describe('safeSendMessage', () => {
  it('resolves with the background response', async () => {
    mockChrome();
    const res = await safeSendMessage({ type: 'ANALYZE_TEXT', payload: { text: 'hi', action: 'grammar' } });
    expect(res).toEqual({ result: 'ok' });
    expect((global.chrome as any).runtime.sendMessage).toHaveBeenCalledWith({
      type: 'ANALYZE_TEXT',
      payload: { text: 'hi', action: 'grammar' },
    });
  });

  it('returns null and fires onContextInvalidated when the context is gone up front', async () => {
    mockChrome({ valid: false });
    const onInvalid = vi.fn();
    const res = await safeSendMessage({ type: 'LIST_MODELS' }, onInvalid);
    expect(res).toBeNull();
    expect(onInvalid).toHaveBeenCalledTimes(1);
  });

  it('returns null and fires onContextInvalidated when sendMessage rejects with an invalidation error', async () => {
    mockChrome();
    (global.chrome as any).runtime.sendMessage = vi
      .fn()
      .mockRejectedValue(new Error('Extension context invalidated.'));
    const onInvalid = vi.fn();
    const res = await safeSendMessage({ type: 'COPY_AS', text: 'x', format: 'markdown' }, onInvalid);
    expect(res).toBeNull();
    expect(onInvalid).toHaveBeenCalledTimes(1);
  });

  it('returns null and warns (no invalidation callback) on other send failures', async () => {
    mockChrome();
    (global.chrome as any).runtime.sendMessage = vi.fn().mockRejectedValue(new Error('boom'));
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const onInvalid = vi.fn();
    const res = await safeSendMessage({ type: 'LIST_MODELS' }, onInvalid);
    expect(res).toBeNull();
    expect(onInvalid).not.toHaveBeenCalled();
    expect(warn).toHaveBeenCalled();
  });
});
