import type { ComponentProps, ReactNode } from 'react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { api } from '@spoon/backend/convex/_generated/api.js';

import { NotificationBell } from '../../src/components/layout/header/controls/notification-bell';

// Stable sentinels: the real Convex `api` proxy hands back a fresh reference on
// every access, so `===` comparisons in the mocks below would never match.
vi.mock('@spoon/backend/convex/_generated/api.js', () => ({
  api: {
    notifications: {
      unreadCount: 'notifications:unreadCount',
      listMine: 'notifications:listMine',
      markRead: 'notifications:markRead',
      markAllRead: 'notifications:markAllRead',
    },
  },
}));

const { mockMarkAllRead, mockMarkRead, mockUseConvexAuth, mockUseQuery } =
  vi.hoisted(() => ({
    mockMarkAllRead: vi.fn(),
    mockMarkRead: vi.fn(),
    mockUseConvexAuth: vi.fn(),
    mockUseQuery: vi.fn(),
  }));

vi.mock('convex/react', () => ({
  useConvexAuth: mockUseConvexAuth,
  useMutation: (reference: unknown) =>
    reference === api.notifications.markAllRead ? mockMarkAllRead : mockMarkRead,
  useQuery: mockUseQuery,
}));

vi.mock('next/navigation', () => ({
  useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));

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

// Render the dropdown primitives as pass-through wrappers so the menu content
// is deterministically present under jsdom (Radix pointer behavior is flaky
// here). We are exercising NotificationBell's logic, not Radix.
vi.mock('@spoon/ui', () => ({
  Button: (props: ComponentProps<'button'>) => <button {...props} />,
  DropdownMenu: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
  DropdownMenuTrigger: ({ children }: { children?: ReactNode }) => <>{children}</>,
  DropdownMenuContent: ({ children }: { children?: ReactNode }) => (
    <div>{children}</div>
  ),
  DropdownMenuItem: ({ children }: { children?: ReactNode }) => <>{children}</>,
  DropdownMenuLabel: ({ children }: { children?: ReactNode }) => (
    <div>{children}</div>
  ),
  DropdownMenuSeparator: () => <hr />,
}));

const notifications = [
  {
    _id: 'notif-1',
    _creationTime: 2,
    userId: 'user-1',
    kind: 'agent_turn_finished',
    title: 'Agent finished its turn',
    body: 'useSend is ready for review.',
    link: '/threads/thread-1',
    createdAt: Date.now() - 60_000,
  },
  {
    _id: 'notif-2',
    _creationTime: 1,
    userId: 'user-1',
    kind: 'sync_failed',
    title: 'Sync failed',
    body: 'Could not sync useSend with upstream.',
    link: '/spoons/spoon-1',
    createdAt: Date.now() - 3_600_000,
  },
];

const seedQueries = (unreadCount: number) => {
  mockUseConvexAuth.mockReturnValue({ isAuthenticated: true, isLoading: false });
  mockUseQuery.mockImplementation((reference: unknown) => {
    if (reference === api.notifications.unreadCount) return unreadCount;
    if (reference === api.notifications.listMine) return notifications;
    return undefined;
  });
};

afterEach(() => {
  cleanup();
  vi.clearAllMocks();
});

describe('NotificationBell', () => {
  it('renders the unread badge with the count', () => {
    seedQueries(3);
    render(<NotificationBell />);
    expect(screen.getByText('3')).toBeInTheDocument();
  });

  it('renders the notification titles when opened', () => {
    seedQueries(3);
    render(<NotificationBell />);
    fireEvent.click(screen.getByRole('button', { name: /notifications/i }));
    expect(screen.getByText('Agent finished its turn')).toBeInTheDocument();
    expect(screen.getByText('Sync failed')).toBeInTheDocument();
  });

  it('marks a notification read when it is clicked', () => {
    seedQueries(3);
    render(<NotificationBell />);
    fireEvent.click(screen.getByRole('button', { name: /notifications/i }));
    fireEvent.click(screen.getByText('Agent finished its turn'));
    expect(mockMarkRead).toHaveBeenCalledWith({ notificationId: 'notif-1' });
  });

  it('marks all read when the action is clicked', () => {
    seedQueries(3);
    render(<NotificationBell />);
    fireEvent.click(screen.getByRole('button', { name: /notifications/i }));
    fireEvent.click(screen.getByRole('button', { name: /mark all read/i }));
    expect(mockMarkAllRead).toHaveBeenCalled();
  });

  it('hides the badge when there are no unread notifications', () => {
    seedQueries(0);
    render(<NotificationBell />);
    expect(screen.queryByText('0')).not.toBeInTheDocument();
  });
});
