import { renderHook, act } from '@testing-library/react';
import { toast } from 'sonner';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { useGscSyncNotification } from './useGscSyncNotification';

/**
 * Tests for the shared GSC-sync browser-notification hook.
 *
 * Coverage focuses on the two failure modes the prior wiring had:
 *
 *   1. The "Notify me when ready" button used to call
 *      `Notification.requestPermission()` directly with no UI feedback —
 *      users had no idea whether their click did anything. The hook's
 *      `requestPermissionWithFeedback()` MUST emit a toast in every branch
 *      (granted / denied / default-then-decided).
 *
 *   2. Even when a user granted permission, no native notification ever
 *      fired on completion outside the OnboardingWizard. The effect inside
 *      the hook MUST fire `new Notification(...)` on the syncing → done
 *      transition, but ONLY when the tab is hidden (the in-page UI is
 *      enough when the user is still looking at the page).
 */

vi.mock('sonner', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    message: vi.fn(),
  },
}));

// Default `document.hidden` to true so the transition path can fire; tests
// that need the visible-tab behavior override per-case.
beforeEach(() => {
  Object.defineProperty(document, 'hidden', {
    value: true,
    writable: true,
    configurable: true,
  });
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.mocked(toast.success).mockClear();
  vi.mocked(toast.error).mockClear();
  vi.mocked(toast.message).mockClear();
  // Clean up any Notification stub between tests so one test's permission
  // state doesn't leak into the next.
  delete (globalThis as { Notification?: unknown }).Notification;
});

/**
 * Install a fake Notification constructor + permission state. Returns the
 * spy so individual tests can assert against `new Notification(...)` calls.
 */
function installNotificationStub(
  permission: NotificationPermission,
  requestPermissionResult?: NotificationPermission,
) {
  const ctor = vi.fn();
  const requestPermission = vi.fn(() =>
    Promise.resolve(requestPermissionResult ?? permission),
  );
  const stub = Object.assign(ctor, {
    permission,
    requestPermission,
  });
  Object.defineProperty(globalThis, 'Notification', {
    value: stub,
    writable: true,
    configurable: true,
  });
  return { ctor, requestPermission };
}

describe('useGscSyncNotification — requestPermissionWithFeedback', () => {
  it('toasts when the browser does not support notifications', () => {
    // No `Notification` global at all.
    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    act(() => result.current.requestPermissionWithFeedback());

    expect(toast.error).toHaveBeenCalledWith(
      'This browser does not support notifications.',
    );
  });

  it('toasts a success message when permission is already granted (no re-prompt)', () => {
    const { requestPermission } = installNotificationStub('granted');

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    act(() => result.current.requestPermissionWithFeedback());

    expect(requestPermission).not.toHaveBeenCalled();
    expect(toast.success).toHaveBeenCalledWith(
      "You'll get a browser notification when sync finishes.",
    );
  });

  it('toasts a blocked-notice when permission is denied', () => {
    const { requestPermission } = installNotificationStub('denied');

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    act(() => result.current.requestPermissionWithFeedback());

    expect(requestPermission).not.toHaveBeenCalled();
    expect(toast.error).toHaveBeenCalledWith(
      'Notifications are blocked. Enable them for this site in your browser settings.',
    );
  });

  it('prompts when permission is default and toasts on grant', async () => {
    const { requestPermission } = installNotificationStub('default', 'granted');

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    await act(async () => {
      result.current.requestPermissionWithFeedback();
      // Flush the resolved promise from requestPermission.
      await Promise.resolve();
    });

    expect(requestPermission).toHaveBeenCalledTimes(1);
    expect(toast.success).toHaveBeenCalledWith(
      "You'll get a browser notification when sync finishes.",
    );
  });

  it('prompts when permission is default and toasts on deny', async () => {
    installNotificationStub('default', 'denied');

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    await act(async () => {
      result.current.requestPermissionWithFeedback();
      await Promise.resolve();
    });

    expect(toast.error).toHaveBeenCalledWith(
      'Notifications blocked. You can re-enable them in your browser settings.',
    );
  });

  it('toasts feedback when the user dismisses the OS prompt without choosing', async () => {
    // 'default' permission, prompt resolves to 'default' again — i.e. the
    // user closed the prompt without picking grant/deny. Previously this
    // path was silent and made the button look broken.
    installNotificationStub('default', 'default');

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    await act(async () => {
      result.current.requestPermissionWithFeedback();
      await Promise.resolve();
    });

    expect(toast.message).toHaveBeenCalledWith(
      "Notifications weren't enabled. Click again to try, or we'll show in-app updates instead.",
    );
    expect(toast.success).not.toHaveBeenCalled();
    expect(toast.error).not.toHaveBeenCalled();
  });

  it('toasts an error when requestPermission() throws synchronously', () => {
    const ctor = vi.fn();
    const requestPermission = vi.fn(() => {
      throw new Error('User gesture required');
    });
    Object.defineProperty(globalThis, 'Notification', {
      value: Object.assign(ctor, {
        permission: 'default' as NotificationPermission,
        requestPermission,
      }),
      writable: true,
      configurable: true,
    });

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    act(() => result.current.requestPermissionWithFeedback());

    expect(toast.error).toHaveBeenCalledWith(
      'Could not request notification permission: User gesture required',
    );
  });

  it('toasts an error when the requestPermission() promise rejects', async () => {
    const ctor = vi.fn();
    const requestPermission = vi.fn(() => Promise.reject(new Error('blocked')));
    Object.defineProperty(globalThis, 'Notification', {
      value: Object.assign(ctor, {
        permission: 'default' as NotificationPermission,
        requestPermission,
      }),
      writable: true,
      configurable: true,
    });

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    await act(async () => {
      result.current.requestPermissionWithFeedback();
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(toast.error).toHaveBeenCalledWith(
      'Could not request notification permission. Check your browser settings.',
    );
  });

  it('falls back to the legacy callback API when requestPermission returns undefined', () => {
    const ctor = vi.fn();
    // Legacy Safari: callback-only, returns undefined, invokes callback
    // synchronously with the resolved permission.
    const requestPermission = vi.fn((cb?: (p: NotificationPermission) => void) => {
      cb?.('granted');
      return undefined as unknown as Promise<NotificationPermission>;
    });
    Object.defineProperty(globalThis, 'Notification', {
      value: Object.assign(ctor, {
        permission: 'default' as NotificationPermission,
        requestPermission,
      }),
      writable: true,
      configurable: true,
    });

    const { result } = renderHook(() =>
      useGscSyncNotification({ isSyncing: true, siteName: 'Example.com' }),
    );

    act(() => result.current.requestPermissionWithFeedback());

    // First call (Promise probe) returns undefined, second call uses the
    // callback form — total of 2 calls.
    expect(requestPermission).toHaveBeenCalledTimes(2);
    expect(toast.success).toHaveBeenCalledWith(
      "You'll get a browser notification when sync finishes.",
    );
  });
});

describe('useGscSyncNotification — completion transition', () => {
  it('fires a native notification when sync transitions from syncing → done with permission granted and tab hidden', () => {
    const { ctor } = installNotificationStub('granted');

    const { rerender } = renderHook(
      ({ isSyncing }: { isSyncing: boolean }) =>
        useGscSyncNotification({ isSyncing, siteName: 'Example.com' }),
      { initialProps: { isSyncing: true } },
    );

    expect(ctor).not.toHaveBeenCalled();

    rerender({ isSyncing: false });

    expect(ctor).toHaveBeenCalledTimes(1);
    expect(ctor).toHaveBeenCalledWith('GSC data is ready!', {
      body: 'Example.com is ready for analysis. Run your first analysis now.',
      icon: '/favicon.ico',
    });
  });

  it('does NOT fire a notification when the tab is visible', () => {
    Object.defineProperty(document, 'hidden', {
      value: false,
      writable: true,
      configurable: true,
    });
    const { ctor } = installNotificationStub('granted');

    const { rerender } = renderHook(
      ({ isSyncing }: { isSyncing: boolean }) =>
        useGscSyncNotification({ isSyncing, siteName: 'Example.com' }),
      { initialProps: { isSyncing: true } },
    );

    rerender({ isSyncing: false });

    expect(ctor).not.toHaveBeenCalled();
  });

  it('does NOT fire a notification when permission was never granted', () => {
    const { ctor } = installNotificationStub('default');

    const { rerender } = renderHook(
      ({ isSyncing }: { isSyncing: boolean }) =>
        useGscSyncNotification({ isSyncing, siteName: 'Example.com' }),
      { initialProps: { isSyncing: true } },
    );

    rerender({ isSyncing: false });

    expect(ctor).not.toHaveBeenCalled();
  });

  it('does not fire when isSyncing starts false (no transition)', () => {
    const { ctor } = installNotificationStub('granted');

    renderHook(() =>
      useGscSyncNotification({ isSyncing: false, siteName: 'Example.com' }),
    );

    expect(ctor).not.toHaveBeenCalled();
  });

  it('falls back to a generic body when siteName is missing', () => {
    const { ctor } = installNotificationStub('granted');

    const { rerender } = renderHook(
      ({ isSyncing }: { isSyncing: boolean }) =>
        useGscSyncNotification({ isSyncing, siteName: null }),
      { initialProps: { isSyncing: true } },
    );

    rerender({ isSyncing: false });

    expect(ctor).toHaveBeenCalledWith('GSC data is ready!', {
      body: 'Your GSC data is ready for analysis.',
      icon: '/favicon.ico',
    });
  });

  it('swallows constructor errors (e.g. Safari without service worker support)', () => {
    const ctor = vi.fn(() => {
      throw new Error('Notification constructor not supported');
    });
    Object.defineProperty(globalThis, 'Notification', {
      value: Object.assign(ctor, {
        permission: 'granted' as NotificationPermission,
        requestPermission: vi.fn(),
      }),
      writable: true,
      configurable: true,
    });

    const { rerender } = renderHook(
      ({ isSyncing }: { isSyncing: boolean }) =>
        useGscSyncNotification({ isSyncing, siteName: 'Example.com' }),
      { initialProps: { isSyncing: true } },
    );

    // The hook MUST NOT throw — Safari support is best-effort.
    expect(() => rerender({ isSyncing: false })).not.toThrow();
    expect(ctor).toHaveBeenCalledTimes(1);
  });
});
