import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";

vi.mock("date-fns", () => ({
  formatDistanceToNow: (): string => "5 minutes ago",
}));

interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: ReactNode;
}

interface MockContainerProps extends HTMLAttributes<HTMLElement> {
  children: ReactNode;
}

interface MockEventSourceInstance {
  url: string;
  onerror: ((event: Event) => void) | null;
  onmessage: ((event: MessageEvent<string>) => void) | null;
  close: ReturnType<typeof vi.fn>;
  emitMessage: (payload: unknown) => void;
}

interface QueueNotification {
  id: string;
  agent: string;
  filename: string;
  payload: unknown;
  createdAt: string;
}

const mockApiGet = vi.fn<(endpoint: string) => Promise<QueueNotification[]>>();
const mockApiPost = vi.fn<(endpoint: string) => Promise<{ success: true; id: string }>>();
const mockShowToast = vi.fn<(message: string, variant?: string) => void>();

let mockEventSourceInstances: MockEventSourceInstance[] = [];

const MockEventSource = vi.fn(function (this: MockEventSourceInstance, url: string): void {
  this.url = url;
  this.onerror = null;
  this.onmessage = null;
  this.close = vi.fn();
  this.emitMessage = (payload: unknown): void => {
    this.onmessage?.(new MessageEvent("message", { data: JSON.stringify(payload) }));
  };

  mockEventSourceInstances.push(this);
});

vi.mock("@/lib/api/client", () => ({
  apiGet: (endpoint: string): Promise<QueueNotification[]> => mockApiGet(endpoint),
  apiPost: (endpoint: string): Promise<{ success: true; id: string }> => mockApiPost(endpoint),
}));

vi.mock("@mosaic/ui", () => ({
  useToast: (): { showToast: typeof mockShowToast; removeToast: ReturnType<typeof vi.fn> } => ({
    showToast: mockShowToast,
    removeToast: vi.fn(),
  }),
}));

vi.mock("@/components/ui/button", () => ({
  Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
    <button {...props}>{children}</button>
  ),
}));

vi.mock("@/components/ui/badge", () => ({
  Badge: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <span {...props}>{children}</span>
  ),
}));

vi.mock("@/components/ui/card", () => ({
  Card: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <section {...props}>{children}</section>
  ),
  CardHeader: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <header {...props}>{children}</header>
  ),
  CardContent: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <div {...props}>{children}</div>
  ),
  CardTitle: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <h2 {...props}>{children}</h2>
  ),
}));

vi.mock("@/components/ui/collapsible", () => ({
  Collapsible: ({ children }: MockContainerProps): React.JSX.Element => <div>{children}</div>,
}));

vi.mock("@/components/ui/scroll-area", () => ({
  ScrollArea: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
    <div {...props}>{children}</div>
  ),
}));

vi.mock("@/components/ui/skeleton", () => ({
  Skeleton: (props: HTMLAttributes<HTMLDivElement>): React.JSX.Element => <div {...props} />,
}));

import { QueueNotificationFeed } from "./QueueNotificationFeed";

function latestEventSource(): MockEventSourceInstance {
  const instance = mockEventSourceInstances[mockEventSourceInstances.length - 1];
  if (!instance) {
    throw new Error("Expected an EventSource instance");
  }
  return instance;
}

function makeNotification(overrides: Partial<QueueNotification>): QueueNotification {
  return {
    id: "notif-1",
    agent: "mosaic",
    filename: "notif-1.json",
    payload: {
      taskId: "MS24-WEB-001",
      eventType: "task.ready",
    },
    createdAt: "2026-03-08T23:00:00.000Z",
    ...overrides,
  };
}

describe("QueueNotificationFeed", (): void => {
  beforeEach((): void => {
    vi.clearAllMocks();
    vi.stubGlobal("EventSource", MockEventSource);
    vi.stubGlobal("fetch", vi.fn());
    mockEventSourceInstances = [];
    mockApiGet.mockResolvedValue([]);
    mockApiPost.mockResolvedValue({ success: true, id: "notif-1" });
  });

  afterEach((): void => {
    vi.unstubAllGlobals();
  });

  it("loads and renders notifications grouped by agent", async (): Promise<void> => {
    mockApiGet.mockResolvedValue([
      makeNotification({ id: "notif-1", agent: "mosaic" }),
      makeNotification({
        id: "notif-2",
        agent: "dyor",
        payload: { taskId: "MS24-WEB-002", eventType: "task.blocked" },
      }),
    ]);

    render(<QueueNotificationFeed />);

    await waitFor((): void => {
      expect(mockApiGet).toHaveBeenCalledWith("/api/orchestrator/api/queue/notifications");
    });

    expect(screen.getByText("mosaic")).toBeInTheDocument();
    expect(screen.getByText("dyor")).toBeInTheDocument();
    expect(screen.getByText("MS24-WEB-001")).toBeInTheDocument();
    expect(screen.getByText("task.ready")).toBeInTheDocument();
    expect(screen.getAllByText("5 minutes ago")).toHaveLength(2);
    expect(MockEventSource).toHaveBeenCalledWith("/api/orchestrator/queue/notifications/stream");
  });

  it("acknowledges a notification and removes it from the list", async (): Promise<void> => {
    const user = userEvent.setup();
    mockApiGet.mockResolvedValue([makeNotification({ id: "notif-ack" })]);

    render(<QueueNotificationFeed />);

    await waitFor((): void => {
      expect(screen.getByText("MS24-WEB-001")).toBeInTheDocument();
    });

    await user.click(screen.getByRole("button", { name: "ACK notification MS24-WEB-001" }));

    await waitFor((): void => {
      expect(mockApiPost).toHaveBeenCalledWith(
        "/api/orchestrator/api/queue/notifications/notif-ack/ack"
      );
    });

    await waitFor((): void => {
      expect(screen.getByText("No pending notifications")).toBeInTheDocument();
    });
  });

  it("renders the empty state when there are no notifications", async (): Promise<void> => {
    mockApiGet.mockResolvedValue([]);

    render(<QueueNotificationFeed />);

    await waitFor((): void => {
      expect(screen.getByText("No pending notifications")).toBeInTheDocument();
    });
  });

  it("refreshes the list when an SSE message arrives", async (): Promise<void> => {
    mockApiGet.mockResolvedValue([makeNotification({ id: "notif-before" })]);

    render(<QueueNotificationFeed />);

    await waitFor((): void => {
      expect(screen.getByText("MS24-WEB-001")).toBeInTheDocument();
    });

    act(() => {
      latestEventSource().emitMessage([
        makeNotification({
          id: "notif-after",
          agent: "sage",
          payload: { taskId: "MS24-WEB-003", eventType: "task.failed" },
        }),
      ]);
    });

    await waitFor((): void => {
      expect(screen.getByText("sage")).toBeInTheDocument();
      expect(screen.getByText("MS24-WEB-003")).toBeInTheDocument();
      expect(screen.getByText("task.failed")).toBeInTheDocument();
    });
  });
});
