import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as MosaicUi from "@mosaic/ui";
import type { ButtonHTMLAttributes, ReactNode } from "react";

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

const mockApiPost = vi.fn<(endpoint: string, body?: unknown) => Promise<{ message?: string }>>();
const mockShowToast = vi.fn<(message: string, variant?: string) => void>();
const useToastSpy = vi.spyOn(MosaicUi, "useToast");

vi.mock("@/lib/api/client", () => ({
  apiPost: (endpoint: string, body?: unknown): Promise<{ message?: string }> =>
    mockApiPost(endpoint, body),
}));

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

import { BargeInInput } from "./BargeInInput";

describe("BargeInInput", (): void => {
  beforeEach((): void => {
    vi.clearAllMocks();
    vi.stubGlobal("fetch", vi.fn());
    mockApiPost.mockResolvedValue({ message: "ok" });
    useToastSpy.mockReturnValue({
      showToast: mockShowToast,
      removeToast: vi.fn(),
    } as ReturnType<typeof MosaicUi.useToast>);
  });

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

  it("renders input controls and keeps send disabled for empty content", (): void => {
    render(<BargeInInput sessionId="session-1" />);

    expect(screen.getByLabelText("Inject message")).toBeInTheDocument();
    expect(screen.getByRole("checkbox", { name: "Pause before send" })).not.toBeChecked();
    expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
  });

  it("sends a trimmed message and clears the textarea", async (): Promise<void> => {
    const onSent = vi.fn<() => void>();
    const user = userEvent.setup();

    render(<BargeInInput sessionId="session-1" onSent={onSent} />);

    const textarea = screen.getByLabelText("Inject message");
    await user.type(textarea, "  execute plan  ");
    await user.click(screen.getByRole("button", { name: "Send" }));

    await waitFor((): void => {
      expect(mockApiPost).toHaveBeenCalledWith(
        "/api/orchestrator/api/mission-control/sessions/session-1/inject",
        {
          content: "execute plan",
        }
      );
    });

    expect(onSent).toHaveBeenCalledTimes(1);
    expect(textarea).toHaveValue("");
  });

  it("pauses and resumes the session around injection when checkbox is enabled", async (): Promise<void> => {
    const user = userEvent.setup();

    render(<BargeInInput sessionId="session-2" />);

    await user.click(screen.getByRole("checkbox", { name: "Pause before send" }));
    await user.type(screen.getByLabelText("Inject message"), "hello world");
    await user.click(screen.getByRole("button", { name: "Send" }));

    await waitFor((): void => {
      expect(mockApiPost).toHaveBeenCalledTimes(3);
    });

    const calls = mockApiPost.mock.calls as [string, unknown?][];

    expect(calls[0]).toEqual([
      "/api/orchestrator/api/mission-control/sessions/session-2/pause",
      undefined,
    ]);
    expect(calls[1]).toEqual([
      "/api/orchestrator/api/mission-control/sessions/session-2/inject",
      { content: "hello world" },
    ]);
    expect(calls[2]).toEqual([
      "/api/orchestrator/api/mission-control/sessions/session-2/resume",
      undefined,
    ]);
  });

  it("submits with Enter and does not submit on Shift+Enter", async (): Promise<void> => {
    const user = userEvent.setup();

    render(<BargeInInput sessionId="session-3" />);

    const textarea = screen.getByLabelText("Inject message");
    await user.type(textarea, "first");
    fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: true });

    expect(mockApiPost).not.toHaveBeenCalled();

    fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: false });

    await waitFor((): void => {
      expect(mockApiPost).toHaveBeenCalledWith(
        "/api/orchestrator/api/mission-control/sessions/session-3/inject",
        {
          content: "first",
        }
      );
    });
  });

  it("shows an inline error and toast when injection fails", async (): Promise<void> => {
    const user = userEvent.setup();
    mockApiPost.mockRejectedValueOnce(new Error("Injection failed"));

    render(<BargeInInput sessionId="session-4" />);

    await user.type(screen.getByLabelText("Inject message"), "help");
    await user.click(screen.getByRole("button", { name: "Send" }));

    await waitFor((): void => {
      expect(screen.getByRole("alert")).toHaveTextContent("Injection failed");
    });

    expect(mockShowToast).toHaveBeenCalledWith("Injection failed", "error");
  });

  it("reports resume failures after a successful send", async (): Promise<void> => {
    const user = userEvent.setup();

    mockApiPost
      .mockResolvedValueOnce({ message: "paused" })
      .mockResolvedValueOnce({ message: "sent" })
      .mockRejectedValueOnce(new Error("resume failed"));

    render(<BargeInInput sessionId="session-5" />);

    await user.click(screen.getByRole("checkbox", { name: "Pause before send" }));
    await user.type(screen.getByLabelText("Inject message"), "deploy now");
    await user.click(screen.getByRole("button", { name: "Send" }));

    await waitFor((): void => {
      expect(screen.getByRole("alert")).toHaveTextContent(
        "Message sent, but failed to resume session: resume failed"
      );
    });

    expect(mockShowToast).toHaveBeenCalledWith(
      "Message sent, but failed to resume session: resume failed",
      "error"
    );
  });
});
