import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type PointerEvent as ReactPointerEvent,
  type KeyboardEvent as ReactKeyboardEvent,
} from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import { Reply, Check, ExternalLink, X, GripVertical, Send } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
import {
  colorBg,
  colorAccent,
  colorAvatarBg,
  colorAvatarRing,
  colorPing,
  useMarkNoteRead,
  useReplyToNote,
  useToggleChecklistItem,
  userDisplayName,
  type ChecklistItem,
  type UserSummary,
} from "@/lib/notes-api";
import {
  useIncomingNotePopup,
  type PopupPayload,
} from "@/contexts/IncomingNotePopupContext";
import { useAuth } from "@/contexts/AuthContext";

function avatarInitial(u: UserSummary | null, lang: string): string {
  const name = userDisplayName(u, lang);
  return name.trim().charAt(0).toUpperCase() || "?";
}

interface StoredPos {
  x: number;
  y: number;
}

const CARD_WIDTH = 384;
const CARD_HEIGHT_ESTIMATE = 280;
const VIEWPORT_MARGIN = 8;
const TOP_INSET = 80;
const SIDE_INSET = 24;

function storageKey(userId: number | null | undefined): string {
  return `txos:floating-note-popup:pos:${userId ?? "anon"}`;
}

function loadStoredPos(userId: number | null | undefined): StoredPos | null {
  if (typeof window === "undefined") return null;
  try {
    const raw = window.sessionStorage.getItem(storageKey(userId));
    if (!raw) return null;
    const parsed = JSON.parse(raw) as Partial<StoredPos>;
    if (typeof parsed.x !== "number" || typeof parsed.y !== "number") return null;
    return { x: parsed.x, y: parsed.y };
  } catch {
    return null;
  }
}

function saveStoredPos(
  userId: number | null | undefined,
  pos: StoredPos,
): void {
  if (typeof window === "undefined") return;
  try {
    window.sessionStorage.setItem(storageKey(userId), JSON.stringify(pos));
  } catch {
    /* ignore quota / disabled storage */
  }
}

/**
 * Default anchor when the user hasn't dragged the card yet.
 * RTL → top-left edge of the viewport; LTR → top-right edge. Keeps
 * the card clear of typical header bars (top inset of 80px).
 */
function defaultAnchor(isRtl: boolean): StoredPos {
  if (typeof window === "undefined") return { x: SIDE_INSET, y: TOP_INSET };
  const x = isRtl
    ? SIDE_INSET
    : Math.max(SIDE_INSET, window.innerWidth - CARD_WIDTH - SIDE_INSET);
  return { x, y: TOP_INSET };
}

function clampToViewport(pos: StoredPos): StoredPos {
  if (typeof window === "undefined") return pos;
  const maxX = Math.max(
    VIEWPORT_MARGIN,
    window.innerWidth - CARD_WIDTH - VIEWPORT_MARGIN,
  );
  const maxY = Math.max(
    VIEWPORT_MARGIN,
    window.innerHeight - CARD_HEIGHT_ESTIMATE - VIEWPORT_MARGIN,
  );
  return {
    x: Math.min(Math.max(VIEWPORT_MARGIN, pos.x), maxX),
    y: Math.min(Math.max(VIEWPORT_MARGIN, pos.y), maxY),
  };
}

/**
 * Read-only checklist render that mirrors the visual style of the
 * NoteCard's `ChecklistView` (notes.tsx). Inlined here to keep the
 * popup self-contained — the popup is purely a preview surface, so
 * the checkboxes are not interactive (toggling is reserved for the
 * full Notes page after "Open note").
 */
// collaborative checklist render. Owners + active recipients
// can toggle items right from the popup; the change persists server-
// side and fans out to other audience members via socket. We stop
// pointer/click propagation so toggling a checkbox doesn't bubble up to
// the popup's own dismiss/drag handlers.
function PopupChecklistPreview({
  items,
  noteId,
  noteColor,
}: {
  items: ChecklistItem[];
  noteId: number;
  // Match the checked-checkbox accent to the parent note's color so the
  // checkbox tint belongs to the card (pink note → pink check, etc.).
  noteColor?: string | null;
}) {
  const accent = colorAccent(noteColor);
  const toggle = useToggleChecklistItem();
  // The server intentionally does NOT echo `note_checklist_changed`
  // back to the actor, so the popup's queued snapshot would stay stale
  // for our own toggles. We patch the queue ourselves with the
  // authoritative `items` from the mutation response so the popup's
  // source-of-truth reflects what we just persisted.
  const { updateChecklistItems } = useIncomingNotePopup();
  // The popup's `items` come from the original socket payload (snapshot
  // at enqueue time), NOT from the React Query cache — so optimistic
  // cache updates won't flip the checkbox here. We keep a local
  // override per item so the user gets instant visual feedback while
  // the mutation is in flight. Items not in the override fall back to
  // the snapshot's `done` value.
  const [overrides, setOverrides] = useState<Record<string, boolean>>({});
  // Reconcile the local overrides whenever the authoritative `items`
  // catches up to (or supersedes) them — otherwise a collaborator's
  // later toggle on the same item could be masked by our stale local
  // override. We drop any override whose value matches the incoming
  // authoritative `done` (the optimistic update has landed) AND any
  // override for an item we no longer recognise.
  useEffect(() => {
    setOverrides((prev) => {
      let changed = false;
      const next: Record<string, boolean> = {};
      for (const [id, val] of Object.entries(prev)) {
        const auth = items.find((it) => it.id === id);
        if (!auth) {
          changed = true;
          continue;
        }
        if (auth.done === val) {
          changed = true;
          continue;
        }
        next[id] = val;
      }
      return changed ? next : prev;
    });
  }, [items]);
  if (!items || items.length === 0) return null;
  return (
    <div
      className="space-y-1 mt-2"
      data-testid="incoming-note-popup-checklist"
      onClick={(e) => e.stopPropagation()}
      onPointerDown={(e) => e.stopPropagation()}
    >
      {items.map((it) => {
        const checked = overrides[it.id] ?? it.done;
        return (
          <label
            key={it.id}
            className="flex items-start gap-2 text-sm cursor-pointer"
            data-testid={`incoming-note-popup-checklist-row-${it.id}`}
          >
            <input
              type="checkbox"
              checked={checked}
              onChange={(e) => {
                const next = e.target.checked;
                setOverrides((o) => ({ ...o, [it.id]: next }));
                toggle.mutate(
                  { id: noteId, itemId: it.id, done: next },
                  {
                    onSuccess: (data) => {
                      // Patch the popup queue with the authoritative
                      // items so the next render reads the saved value
                      // from `items` (the override-reconciler effect
                      // will then drop the matching override).
                      if (Array.isArray(data?.items)) {
                        updateChecklistItems(noteId, data.items);
                      } else {
                        setOverrides((o) => {
                          const { [it.id]: _drop, ...rest } = o;
                          return rest;
                        });
                      }
                    },
                    onError: () =>
                      // Roll back the local override on failure so the
                      // checkbox snaps back to authoritative state.
                      setOverrides((o) => {
                        const { [it.id]: _drop, ...rest } = o;
                        return rest;
                      }),
                  },
                );
              }}
              className={`mt-0.5 h-4 w-4 rounded border-black/30 ${accent}`}
              data-testid={`incoming-note-popup-checklist-check-${it.id}`}
            />
            <span
              className={`flex-1 break-words ${
                checked ? "line-through decoration-foreground/60 text-foreground/55" : "text-foreground"
              }`}
            >
              {it.text || "\u00A0"}
            </span>
          </label>
        );
      })}
    </div>
  );
}

export function IncomingNotePopup() {
  const { t, i18n } = useTranslation();
  const isRtl = i18n.language === "ar";
  const [, setLocation] = useLocation();
  const { current, queueLength, dismiss } = useIncomingNotePopup();
  const markRead = useMarkNoteRead();
  const replyMutation = useReplyToNote();
  const { user } = useAuth();
  const userId = user?.id ?? null;

  // Inline reply composer state. When `composerOpen` is true the action
  // row is replaced with a textarea + Send/Cancel so the user can reply
  // without navigating away from the current page.
  const [composerOpen, setComposerOpen] = useState(false);
  const [replyText, setReplyText] = useState("");
  const [justSent, setJustSent] = useState(false);
  const replyTextareaRef = useRef<HTMLTextAreaElement | null>(null);

  const [position, setPosition] = useState<StoredPos>(() =>
    clampToViewport(loadStoredPos(userId) ?? defaultAnchor(isRtl)),
  );
  const [enterAnim, setEnterAnim] = useState(false);
  // Reactive flag mirrored from `dragRef`. The ref drives per-pixel
  // pointermove updates without re-rendering, but we still need a
  // reactive bit so React turns OFF the transform transition during
  // a drag — otherwise pointermove writes race the CSS transition
  // and the card stutters.
  const [isDragging, setIsDragging] = useState(false);

  // Pointer-event drag state. Kept in a ref so dragging doesn't churn
  // through React renders on every pointermove — only the inline style
  // is mutated until pointerup, when we commit the final position.
  const dragRef = useRef<{
    pointerId: number;
    startClientX: number;
    startClientY: number;
    startPosX: number;
    startPosY: number;
  } | null>(null);
  const cardRef = useRef<HTMLDivElement | null>(null);

  // Re-anchor when identity / language direction changes so the previous
  // user's stored position doesn't leak into a new session and the
  // default corner flips correctly on LTR↔RTL switch.
  useEffect(() => {
    setPosition(clampToViewport(loadStoredPos(userId) ?? defaultAnchor(isRtl)));
    // Intentional: only re-anchor on identity / direction change.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId, isRtl]);

  // Trigger the "scale-in + fade" enter animation on mount of each new
  // popup entry. We toggle from `false → true` on the next paint so the
  // CSS transition has a starting state to interpolate from.
  useEffect(() => {
    if (!current) return;
    setEnterAnim(false);
    const id = requestAnimationFrame(() => setEnterAnim(true));
    return () => cancelAnimationFrame(id);
  }, [current]);

  // Keep the card on-screen if the viewport shrinks (rotate / resize).
  useEffect(() => {
    if (typeof window === "undefined") return;
    const onResize = () => setPosition((p) => clampToViewport(p));
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  const handleDismissCurrent = useCallback(() => {
    if (!current) return;
    if (current.kind === "reply") {
      dismiss({ replyId: current.replyId });
    } else {
      dismiss(current.noteId);
    }
  }, [current, dismiss]);

  // ESC dismisses the current entry (parity with the old modal).
  // While the inline reply composer is open, Esc instead cancels the
  // composer — regardless of which control inside the popup currently
  // holds focus (textarea, Send button, Cancel button) — so a stray
  // Esc never throws away the whole popup along with a half-typed
  // reply.
  useEffect(() => {
    if (!current) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== "Escape") return;
      if (composerOpen) {
        if (replyMutation.isPending || justSent) return;
        e.preventDefault();
        setComposerOpen(false);
        setReplyText("");
        return;
      }
      e.preventDefault();
      handleDismissCurrent();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [
    current,
    composerOpen,
    handleDismissCurrent,
    replyMutation.isPending,
    justSent,
  ]);

  // Reset the inline composer whenever the popup head changes (a new
  // note/reply rotates in, or the queue empties) so a half-typed reply
  // never leaks across payloads.
  const currentKey = current
    ? current.kind === "reply"
      ? `reply:${current.replyId}`
      : `note:${current.noteId}`
    : null;
  useEffect(() => {
    setComposerOpen(false);
    setReplyText("");
    setJustSent(false);
  }, [currentKey]);

  const isReply = current?.kind === "reply";
  const replyPayload = isReply
    ? (current as Extract<PopupPayload, { kind: "reply" }>)
    : null;
  const notePayload =
    current && current.kind !== "reply"
      ? (current as Extract<PopupPayload, { kind?: "note" }>)
      : null;

  const senderForHeader: UserSummary | null = useMemo(() => {
    if (!current) return null;
    if (replyPayload) return replyPayload.replier;
    return current.sender;
  }, [current, replyPayload]);

  const senderName = senderForHeader
    ? userDisplayName(senderForHeader, i18n.language)
    : null;

  const heading = useMemo(() => {
    if (!current) return "";
    if (replyPayload) {
      return senderName
        ? t("notes.popup.replyHeading", { name: senderName })
        : t("notes.popup.replyHeadingNoSender");
    }
    return senderName
      ? t("notes.popup.heading", { name: senderName })
      : t("notes.popup.headingNoSender");
  }, [current, replyPayload, senderName, t]);

  if (!current) return null;

  ///#470: derive accent classes from the note's own color so
  // the popup looks like an extension of the note (pink note → pink
  // ping + pink avatar tint, etc.) instead of the previous fixed amber
  // chrome. The outer ring was removed in #470 — only the avatar bg,
  // avatar ring and ping pulse still pick up the note color.
  const noteColor = current.color;
  const avatarBgClass = colorAvatarBg(noteColor);
  const avatarRingClass = colorAvatarRing(noteColor);
  const pingClass = colorPing(noteColor);

  const acknowledge = () => {
    // Only the recipient (note variant) marks-as-read. The reply
    // variant goes to the *original sender* (note owner), who can't
    // mark their own outbound note read.
    if (!replyPayload) {
      markRead.mutate(current.noteId);
    }
  };

  const handleDismiss = () => handleDismissCurrent();
  const handleMarkRead = () => {
    acknowledge();
    handleDismiss();
  };
  // Open the inline reply composer in place of the action row. We
  // intentionally do NOT acknowledge (mark-as-read) yet — that happens
  // on successful send, so a user who opens the composer then cancels
  // doesn't silently flip the note to "read".
  const handleReply = () => {
    setComposerOpen(true);
    // Defer focus until the textarea is mounted on the next paint.
    requestAnimationFrame(() => {
      replyTextareaRef.current?.focus();
    });
  };
  const handleCancelReply = () => {
    setComposerOpen(false);
    setReplyText("");
  };
  const handleSubmitReply = () => {
    if (!current) return;
    const content = replyText.trim();
    if (!content) return;
    if (replyMutation.isPending) return;
    // Resolve the reply target. Note-variant: I'm a recipient, the
    // server infers the recipient (the original sender) so we leave
    // recipientUserId undefined. Reply-variant: I'm the original
    // owner replying back to the specific replier, so we must pass
    // the replier's user id (the thread reply route requires it for
    // owners with multiple recipients).
    const recipientUserId = replyPayload?.replier?.id;
    replyMutation.mutate(
      { id: current.noteId, content, recipientUserId },
      {
        onSuccess: () => {
          // Mark-as-read for note variant only (mirrors acknowledge()
          // semantics — owners can't mark their own outbound note read).
          acknowledge();
          setReplyText("");
          setJustSent(true);
          // Brief on-card "Sent" confirmation before the popup
          // dismisses, so the user gets visible feedback without a
          // toast taking the user's attention away from the page.
          window.setTimeout(() => {
            handleDismissCurrent();
          }, 700);
        },
        onError: () => {
          toast({
            title: t("notes.popup.replyFailed"),
            variant: "destructive",
          });
        },
      },
    );
  };
  const onComposerKeyDown = (
    e: ReactKeyboardEvent<HTMLTextAreaElement>,
  ) => {
    // Esc is handled by the global window listener (which cancels the
    // composer regardless of the focused control). We deliberately do
    // NOT intercept it here so Esc behaves identically whether the
    // textarea or one of the buttons holds focus.
    //
    // Send on Enter (or Cmd/Ctrl+Enter); Shift+Enter inserts a
    // newline so the user can still compose multi-line replies.
    if (e.key === "Enter" && !e.shiftKey && !e.altKey) {
      e.preventDefault();
      handleSubmitReply();
    }
  };
  const handleOpen = () => {
    acknowledge();
    const noteId = current.noteId;
    handleDismiss();
    setLocation(`/notes?thread=${noteId}`);
  };

  // ---- Drag handlers ----
  // Only the header (`.drag-handle`) starts a drag. The card itself
  // is positioned `fixed` and updated via inline style during move; on
  // pointerup the final position is committed to React state +
  // sessionStorage. Buttons inside the body get their own clicks
  // because we never call preventDefault on them.
  const onHandlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
    if (e.button !== 0 && e.pointerType === "mouse") return;
    const target = e.currentTarget;
    target.setPointerCapture(e.pointerId);
    dragRef.current = {
      pointerId: e.pointerId,
      startClientX: e.clientX,
      startClientY: e.clientY,
      startPosX: position.x,
      startPosY: position.y,
    };
    setIsDragging(true);
  };

  const onHandlePointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (!drag || drag.pointerId !== e.pointerId) return;
    const next = clampToViewport({
      x: drag.startPosX + (e.clientX - drag.startClientX),
      y: drag.startPosY + (e.clientY - drag.startClientY),
    });
    if (cardRef.current) {
      cardRef.current.style.transform = `translate3d(${next.x}px, ${next.y}px, 0)`;
    }
  };

  const onHandlePointerEnd = (e: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (!drag || drag.pointerId !== e.pointerId) return;
    const target = e.currentTarget;
    if (target.hasPointerCapture(e.pointerId)) {
      target.releasePointerCapture(e.pointerId);
    }
    const next = clampToViewport({
      x: drag.startPosX + (e.clientX - drag.startClientX),
      y: drag.startPosY + (e.clientY - drag.startClientY),
    });
    dragRef.current = null;
    setIsDragging(false);
    setPosition(next);
    saveStoredPos(userId, next);
  };

  const bodyContent = replyPayload
    ? replyPayload.replyContent
    : current.content;
  // Render the inner sticky-note as faithfully as the NoteCard does:
  // text body for "text" notes, interactive collaborative checklist for
  // "checklist".
  // Reply-variant popups always render the reply text as plain content.
  const isChecklistNote =
    !!notePayload &&
    notePayload.noteKind === "checklist" &&
    Array.isArray(notePayload.items);
  const checklistItems = isChecklistNote
    ? (notePayload!.items as ChecklistItem[])
    : [];

  return (
    // The popup is "floating", not modal: render the card directly as
    // a fixed-positioned element. We deliberately AVOID a full-viewport
    // wrapper — on iOS Safari a high-z `fixed inset-0` layer (even with
    // `pointer-events-none`) interacts badly with stacked floating
    // panels and intermittently swallows the next tap on whatever sits
    // underneath. Sizing the wrapper to just the card fixes that.
    <div
      ref={cardRef}
      key={replyPayload ? `reply-${replyPayload.replyId}` : `note-${current.noteId}`}
      role="dialog"
      aria-label={heading}
      data-testid="incoming-note-popup"
      data-popup-kind={replyPayload ? "reply" : "note"}
      dir={isRtl ? "rtl" : "ltr"}
      className="fixed top-0 left-0 z-[110]"
      style={{
        width: CARD_WIDTH,
        maxWidth: "calc(100vw - 16px)",
        transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
        transition: isDragging
          ? "none"
          : "transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 200ms ease-out",
        opacity: enterAnim ? 1 : 0,
        willChange: "transform",
      }}
    >
        <div
          className={`rounded-none shadow-2xl overflow-hidden transition-transform duration-200 ${
            noteColor === "default"
              ? "bg-background"
              : colorBg(noteColor)
          } ${enterAnim ? "scale-100" : "scale-95"}`}
          data-popup-color={current.color}
        >
          {/* Drag handle / header */}
          <div
            onPointerDown={onHandlePointerDown}
            onPointerMove={onHandlePointerMove}
            onPointerUp={onHandlePointerEnd}
            onPointerCancel={onHandlePointerEnd}
            className="flex items-center gap-3 px-4 py-3 cursor-grab active:cursor-grabbing select-none touch-none"
            data-testid="incoming-note-popup-drag-handle"
            title={t("notes.popup.dragHint")}
          >
            <GripVertical
              size={16}
              className="text-muted-foreground shrink-0"
              aria-hidden="true"
            />
            <span className="relative shrink-0">
              <span
                className={`absolute inset-0 rounded-full ${pingClass} animate-ping`}
                aria-hidden="true"
              />
              {senderForHeader?.avatarUrl ? (
                <img
                  src={senderForHeader.avatarUrl}
                  alt=""
                  className={`relative h-8 w-8 rounded-full object-cover ring-2 ${avatarRingClass}`}
                />
              ) : (
                <div className={`relative h-8 w-8 rounded-full ${avatarBgClass} text-sm font-semibold flex items-center justify-center ring-2 ${avatarRingClass}`}>
                  {avatarInitial(senderForHeader, i18n.language)}
                </div>
              )}
            </span>
            <div className="flex-1 min-w-0">
              <div className="text-sm font-semibold truncate">{heading}</div>
            </div>
            <button
              type="button"
              onClick={handleDismiss}
              onPointerDown={(e) => e.stopPropagation()}
              className="text-muted-foreground hover:text-foreground rounded-md p-1"
              aria-label={t("notes.popup.dismiss")}
              data-testid="incoming-note-popup-close"
            >
              <X size={16} />
            </button>
          </div>

          {/* Body — renders directly on the tinted card surface. */}
          <div
            className="p-4 space-y-3 max-h-[50vh] overflow-y-auto"
            data-testid="incoming-note-popup-body"
          >
            {isChecklistNote ? (
              <PopupChecklistPreview
                items={checklistItems}
                noteId={current.noteId}
                noteColor={current.color}
              />
            ) : (
              bodyContent && (
                <div className="text-sm text-foreground/80 whitespace-pre-wrap break-words">
                  {bodyContent}
                </div>
              )
            )}

            {queueLength > 1 && (
              <div
                className="text-xs text-muted-foreground"
                data-testid="incoming-note-popup-queue"
              >
                {t("notes.popup.queueMore", { count: queueLength - 1 })}
              </div>
            )}
          </div>

          {/* Action row — collapses into the inline reply composer
              when the user clicks "Reply" so they can respond without
              leaving the current page. */}
          <div className="px-4 py-3">
            {composerOpen ? (
              <div
                className="space-y-2"
                data-testid="incoming-note-popup-inline-reply"
                onPointerDown={(e) => e.stopPropagation()}
                onClick={(e) => e.stopPropagation()}
              >
                <textarea
                  ref={replyTextareaRef}
                  value={replyText}
                  onChange={(e) => setReplyText(e.target.value)}
                  onKeyDown={onComposerKeyDown}
                  rows={3}
                  placeholder={t("notes.popup.inlineReplyPlaceholder")}
                  disabled={replyMutation.isPending || justSent}
                  className="w-full resize-y rounded-md border border-black/15 bg-background/80 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-black/20"
                  data-testid="incoming-note-popup-inline-reply-input"
                  dir="auto"
                />
                <div className="flex items-center justify-between gap-2">
                  <div
                    className="text-xs text-muted-foreground min-h-[1rem]"
                    aria-live="polite"
                    data-testid="incoming-note-popup-inline-reply-status"
                  >
                    {justSent
                      ? t("notes.popup.replySent")
                      : replyMutation.isPending
                        ? t("notes.popup.replySending")
                        : ""}
                  </div>
                  <div className="flex gap-2">
                    <Button
                      type="button"
                      variant="ghost"
                      size="sm"
                      onClick={handleCancelReply}
                      disabled={replyMutation.isPending || justSent}
                      data-testid="incoming-note-popup-inline-reply-cancel"
                    >
                      {t("notes.popup.cancelReply")}
                    </Button>
                    <Button
                      type="button"
                      size="sm"
                      onClick={handleSubmitReply}
                      disabled={
                        !replyText.trim() ||
                        replyMutation.isPending ||
                        justSent
                      }
                      data-testid="incoming-note-popup-inline-reply-send"
                    >
                      <Send size={14} className="me-1" />
                      {t("notes.popup.sendReply")}
                    </Button>
                  </div>
                </div>
              </div>
            ) : (
              <div className="flex flex-wrap gap-2 sm:flex-nowrap justify-end">
                <Button
                  type="button"
                  variant="ghost"
                  size="sm"
                  onClick={handleMarkRead}
                  data-testid="incoming-note-popup-mark-read"
                >
                  <Check size={14} className="me-1" />
                  {t("notes.popup.done")}
                </Button>
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  onClick={handleOpen}
                  data-testid="incoming-note-popup-open"
                >
                  <ExternalLink size={14} className="me-1" />
                  {t("notes.popup.openNote")}
                </Button>
                <Button
                  type="button"
                  size="sm"
                  onClick={handleReply}
                  data-testid="incoming-note-popup-reply"
                >
                  <Reply size={14} className="me-1" />
                  {replyPayload
                    ? t("notes.popup.replyBack")
                    : t("notes.popup.reply")}
                </Button>
              </div>
            )}
          </div>
        </div>
      </div>
  );
}
