/**
 * Tests for the generic OAuth service.
 *
 * The CLI launcher (launchOAuthCli) is testable by mocking window globals and
 * simulating postMessage events. The extension launcher requires chrome.runtime
 * and is verified manually.
 */

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

// --- Stub the window global for Node environment ---

const mockPopup = { close: vi.fn() };
const messageListeners = new Set<Function>();

const MOCK_ORIGIN = 'http://localhost';

const mockWindow = {
  open: vi.fn(() => mockPopup),
  addEventListener: vi.fn((type: string, fn: Function) => {
    if (type === 'message') messageListeners.add(fn);
  }),
  removeEventListener: vi.fn((type: string, fn: Function) => {
    if (type === 'message') messageListeners.delete(fn);
  }),
  location: { origin: MOCK_ORIGIN, pathname: '/', search: '' },
};

vi.stubGlobal('window', mockWindow);

// Stub fetch for the server-side polling fallback (returns 204 = no result yet)
vi.stubGlobal(
  'fetch',
  vi.fn(() => Promise.resolve({ status: 204 }))
);

// Default location: standalone CLI (no polling)
vi.stubGlobal('location', { pathname: '/', search: '' });

function fireMessage(data: unknown, opts: { origin?: string; source?: object | null } = {}) {
  const origin = opts.origin ?? MOCK_ORIGIN;
  const source = 'source' in opts ? opts.source : mockPopup;
  for (const handler of messageListeners) {
    handler({ data, origin, source } as unknown as MessageEvent);
  }
}

// Import AFTER stubs are in place (module reads `window` at call time)
import { createOAuthLauncher, getOAuthPageOrigin } from '../../src/providers/oauth-service.js';

describe('createOAuthLauncher', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    messageListeners.clear();
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('returns a function (CLI launcher in Node environment)', () => {
    const launcher = createOAuthLauncher();
    expect(typeof launcher).toBe('function');
  });

  it('opens a popup with the authorize URL', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize?client_id=test');

    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#access_token=abc123',
    });

    const result = await promise;
    expect(mockWindow.open).toHaveBeenCalledWith(
      'https://idp.example.com/authorize?client_id=test',
      '_blank',
      'width=500,height=700,popup=yes'
    );
    expect(result).toBe('http://localhost:5710/auth/callback#access_token=abc123');
  });

  it('returns null when callback reports an error', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    fireMessage({
      type: 'oauth-callback',
      error: 'access_denied',
    });

    const result = await promise;
    expect(result).toBeNull();
  });

  it('ignores unrelated postMessage events', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    // Fire unrelated messages — should be ignored
    fireMessage({ type: 'unrelated-event' });
    fireMessage({ something: 'else' });
    fireMessage(null);

    // Now fire the real callback
    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#token=xyz',
    });

    const result = await promise;
    expect(result).toBe('http://localhost:5710/auth/callback#token=xyz');
  });

  it('returns null on timeout and closes popup', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    // Advance past the 2-minute timeout
    vi.advanceTimersByTime(120001);

    const result = await promise;
    expect(result).toBeNull();
    expect(mockPopup.close).toHaveBeenCalled();
  });

  it('cleans up message listener after successful callback', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#token=abc',
    });

    await promise;

    expect(mockWindow.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
  });

  it('returns null when redirectUrl is missing from callback', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    fireMessage({
      type: 'oauth-callback',
      // no redirectUrl, no error
    });

    const result = await promise;
    expect(result).toBeNull();
  });

  it('resolves to null on timeout when window.open returns null (popup blocked)', async () => {
    mockWindow.open.mockReturnValueOnce(null as any);
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    vi.advanceTimersByTime(120001);

    const result = await promise;
    expect(result).toBeNull();
    // Should not throw when trying to close a null popup
  });

  it('resolves via server-side polling in Electron overlay mode', async () => {
    // Simulate Electron overlay URL
    vi.stubGlobal('location', { pathname: '/electron', search: '' });

    const mockFetch = vi.mocked(fetch);
    // First poll: no result yet
    mockFetch.mockResolvedValueOnce({ status: 204 } as Response);
    // Second poll: result available
    mockFetch.mockResolvedValueOnce({
      status: 200,
      json: () =>
        Promise.resolve({
          redirectUrl: 'http://localhost:5710/auth/callback#token=polled',
        }),
    } as Response);

    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    // Advance past first poll (1s) — returns 204
    await vi.advanceTimersByTimeAsync(1000);
    // Advance past second poll (1s) — returns the result
    await vi.advanceTimersByTimeAsync(1000);

    const result = await promise;
    expect(result).toBe('http://localhost:5710/auth/callback#token=polled');

    // Restore default location
    vi.stubGlobal('location', { pathname: '/', search: '' });
  });

  it('does not poll in standalone CLI mode', async () => {
    vi.stubGlobal('location', { pathname: '/', search: '' });

    const mockFetch = vi.mocked(fetch);

    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    // Advance past where polling would fire
    await vi.advanceTimersByTimeAsync(2000);

    // fetch should NOT have been called (no polling in standalone mode)
    expect(mockFetch).not.toHaveBeenCalled();

    // Resolve via postMessage
    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#token=msg',
    });

    const result = await promise;
    expect(result).toBe('http://localhost:5710/auth/callback#token=msg');
  });

  it('continues polling on server errors in Electron overlay mode', async () => {
    vi.stubGlobal('location', { pathname: '/electron', search: '' });

    const mockFetch = vi.mocked(fetch);
    // First poll: server error
    mockFetch.mockResolvedValueOnce({ status: 500 } as Response);
    // Second poll: success
    mockFetch.mockResolvedValueOnce({
      status: 200,
      json: () =>
        Promise.resolve({
          redirectUrl: 'http://localhost:5710/auth/callback#token=recovered',
        }),
    } as Response);

    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    // First poll — 500 error, caught and retried
    await vi.advanceTimersByTimeAsync(1000);
    // Second poll — success
    await vi.advanceTimersByTimeAsync(1000);

    const result = await promise;
    expect(result).toBe('http://localhost:5710/auth/callback#token=recovered');

    vi.stubGlobal('location', { pathname: '/', search: '' });
  });

  it('does not resolve twice on duplicate callbacks', async () => {
    const launcher = createOAuthLauncher();
    const promise = launcher('https://idp.example.com/authorize');

    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#token=first',
    });

    // Second callback after listener is removed — should be ignored
    fireMessage({
      type: 'oauth-callback',
      redirectUrl: 'http://localhost:5710/auth/callback#token=second',
    });

    const result = await promise;
    expect(result).toBe('http://localhost:5710/auth/callback#token=first');
  });
});

describe('getOAuthPageOrigin', () => {
  const originalWindow = (globalThis as any).window;

  afterEach(() => {
    if (originalWindow === undefined) {
      delete (globalThis as any).window;
    } else {
      (globalThis as any).window = originalWindow;
    }
    delete (globalThis as any).__slicc_panelRpc;
  });

  it('reads origin and href from window when available', async () => {
    (globalThis as any).window = {
      location: { origin: 'http://localhost:5711', href: 'http://localhost:5711/?x=1' },
    };
    const info = await getOAuthPageOrigin();
    expect(info.origin).toBe('http://localhost:5711');
    expect(info.href).toBe('http://localhost:5711/?x=1');
  });

  it('routes through panel-RPC when window is undefined (worker context)', async () => {
    delete (globalThis as any).window;
    const callSpy = vi.fn(async (op: string) => {
      expect(op).toBe('page-info');
      return { origin: 'http://localhost:5731', href: 'http://localhost:5731/foo', title: 't' };
    });
    (globalThis as any).__slicc_panelRpc = { call: callSpy, dispose: () => {} };

    const info = await getOAuthPageOrigin();
    expect(callSpy).toHaveBeenCalled();
    expect(info.origin).toBe('http://localhost:5731');
    expect(info.href).toBe('http://localhost:5731/foo');
  });

  it('throws with a clear message when no window and no panel-RPC bridge', async () => {
    delete (globalThis as any).window;
    delete (globalThis as any).__slicc_panelRpc;
    await expect(getOAuthPageOrigin()).rejects.toThrow(/panel-RPC bridge/);
  });
});
