/**
 * @file ChatInput.test.tsx
 * @description Tests for ChatInput: model selector, temperature/params, localStorage persistence,
 * and command autocomplete.
 */

import { render, screen, fireEvent, waitFor, within, act } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
  ChatInput,
  AVAILABLE_MODELS,
  DEFAULT_MODEL,
  DEFAULT_TEMPERATURE,
  DEFAULT_MAX_TOKENS,
} from "./ChatInput";

// Mock fetch for version.json
beforeEach(() => {
  global.fetch = vi.fn().mockRejectedValue(new Error("Not found"));
});

afterEach(() => {
  vi.restoreAllMocks();
  localStorage.clear();
});

/** Get the first non-default model from the list */
function getNonDefaultModel(): (typeof AVAILABLE_MODELS)[number] {
  const model = AVAILABLE_MODELS.find((m) => m.id !== DEFAULT_MODEL);
  if (!model) throw new Error("No non-default model found");
  return model;
}

describe("ChatInput — model selector", () => {
  it("should render the model selector chip showing the default model", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const defaultLabel =
      AVAILABLE_MODELS.find((m) => m.id === DEFAULT_MODEL)?.label ?? DEFAULT_MODEL;
    expect(screen.getByText(defaultLabel)).toBeDefined();
  });

  it("should open the model dropdown when the chip is clicked", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const chip = screen.getByLabelText(/model:/i);
    fireEvent.click(chip);

    // The dropdown (listbox role) should be visible
    const listbox = screen.getByRole("listbox", { name: /available models/i });
    expect(listbox).toBeDefined();

    // All model options should appear in the dropdown
    const options = within(listbox).getAllByRole("option");
    expect(options.length).toBe(AVAILABLE_MODELS.length);
  });

  it("should call onModelChange when a model is selected", async () => {
    const onModelChange = vi.fn();
    render(<ChatInput onSend={vi.fn()} onModelChange={onModelChange} />);

    const chip = screen.getByLabelText(/model:/i);
    fireEvent.click(chip);

    const targetModel = getNonDefaultModel();
    const listbox = screen.getByRole("listbox", { name: /available models/i });
    const targetOption = within(listbox).getByText(targetModel.label);
    fireEvent.click(targetOption);

    await waitFor(() => {
      const calls = onModelChange.mock.calls.map((c: unknown[]) => c[0]);
      expect(calls).toContain(targetModel.id);
    });
  });

  it("should persist the selected model in localStorage", async () => {
    render(<ChatInput onSend={vi.fn()} />);

    const chip = screen.getByLabelText(/model:/i);
    fireEvent.click(chip);

    const targetModel = getNonDefaultModel();
    const listbox = screen.getByRole("listbox", { name: /available models/i });
    const targetOption = within(listbox).getByText(targetModel.label);
    fireEvent.click(targetOption);

    await waitFor(() => {
      expect(localStorage.getItem("chat:selectedModel")).toBe(targetModel.id);
    });
  });

  it("should restore the model from localStorage on mount", async () => {
    const targetModel = getNonDefaultModel();
    localStorage.setItem("chat:selectedModel", targetModel.id);

    render(<ChatInput onSend={vi.fn()} />);

    await waitFor(() => {
      expect(screen.getByText(targetModel.label)).toBeDefined();
    });
  });

  it("should close the dropdown after selecting a model", async () => {
    render(<ChatInput onSend={vi.fn()} />);

    const chip = screen.getByLabelText(/model:/i);
    fireEvent.click(chip);

    const targetModel = getNonDefaultModel();
    const listbox = screen.getByRole("listbox", { name: /available models/i });
    const targetOption = within(listbox).getByText(targetModel.label);
    fireEvent.click(targetOption);

    // After selection, dropdown should close
    await waitFor(() => {
      expect(screen.queryByRole("listbox")).toBeNull();
    });
  });

  it("should have aria-expanded on the model chip button", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const chip = screen.getByLabelText(/model:/i);
    expect(chip.getAttribute("aria-expanded")).toBe("false");

    fireEvent.click(chip);
    expect(chip.getAttribute("aria-expanded")).toBe("true");
  });
});

describe("ChatInput — temperature and max tokens", () => {
  it("should render the settings/params button", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const settingsBtn = screen.getByLabelText(/chat parameters/i);
    expect(settingsBtn).toBeDefined();
  });

  it("should open the params popover when settings button is clicked", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const settingsBtn = screen.getByLabelText(/chat parameters/i);
    fireEvent.click(settingsBtn);

    expect(screen.getByLabelText(/temperature/i)).toBeDefined();
    expect(screen.getByLabelText(/maximum tokens/i)).toBeDefined();
  });

  it("should show the default temperature value", () => {
    render(<ChatInput onSend={vi.fn()} />);
    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const slider = screen.getByLabelText(/temperature/i);
    expect(parseFloat((slider as HTMLInputElement).value)).toBeCloseTo(DEFAULT_TEMPERATURE);
  });

  it("should call onTemperatureChange when the slider is moved", async () => {
    const onTemperatureChange = vi.fn();
    render(<ChatInput onSend={vi.fn()} onTemperatureChange={onTemperatureChange} />);

    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const slider = screen.getByLabelText(/temperature/i);
    fireEvent.change(slider, { target: { value: "1.2" } });

    await waitFor(() => {
      const calls = onTemperatureChange.mock.calls.map((c: unknown[]) => c[0]);
      expect(calls).toContain(1.2);
    });
  });

  it("should persist temperature in localStorage", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const slider = screen.getByLabelText(/temperature/i);
    fireEvent.change(slider, { target: { value: "0.5" } });

    await waitFor(() => {
      expect(localStorage.getItem("chat:temperature")).toBe("0.5");
    });
  });

  it("should restore temperature from localStorage on mount", async () => {
    localStorage.setItem("chat:temperature", "1.5");

    const onTemperatureChange = vi.fn();
    render(<ChatInput onSend={vi.fn()} onTemperatureChange={onTemperatureChange} />);

    await waitFor(() => {
      const calls = onTemperatureChange.mock.calls.map((c: unknown[]) => c[0]);
      expect(calls).toContain(1.5);
    });
  });

  it("should show the default max tokens value", () => {
    render(<ChatInput onSend={vi.fn()} />);
    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const input = screen.getByLabelText(/maximum tokens/i);
    expect(parseInt((input as HTMLInputElement).value, 10)).toBe(DEFAULT_MAX_TOKENS);
  });

  it("should call onMaxTokensChange when the max tokens input changes", async () => {
    const onMaxTokensChange = vi.fn();
    render(<ChatInput onSend={vi.fn()} onMaxTokensChange={onMaxTokensChange} />);

    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const input = screen.getByLabelText(/maximum tokens/i);
    fireEvent.change(input, { target: { value: "8192" } });

    await waitFor(() => {
      const calls = onMaxTokensChange.mock.calls.map((c: unknown[]) => c[0]);
      expect(calls).toContain(8192);
    });
  });

  it("should persist max tokens in localStorage", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    fireEvent.click(screen.getByLabelText(/chat parameters/i));

    const input = screen.getByLabelText(/maximum tokens/i);
    fireEvent.change(input, { target: { value: "2000" } });

    await waitFor(() => {
      expect(localStorage.getItem("chat:maxTokens")).toBe("2000");
    });
  });

  it("should restore max tokens from localStorage on mount", async () => {
    localStorage.setItem("chat:maxTokens", "8000");

    const onMaxTokensChange = vi.fn();
    render(<ChatInput onSend={vi.fn()} onMaxTokensChange={onMaxTokensChange} />);

    await waitFor(() => {
      const calls = onMaxTokensChange.mock.calls.map((c: unknown[]) => c[0]);
      expect(calls).toContain(8000);
    });
  });
});

describe("ChatInput — externalValue (suggestion fill)", () => {
  it("should update the textarea when externalValue is provided", async () => {
    const { rerender } = render(<ChatInput onSend={vi.fn()} />);

    const textarea = screen.getByLabelText(/message input/i);
    expect((textarea as HTMLTextAreaElement).value).toBe("");

    rerender(<ChatInput onSend={vi.fn()} externalValue="Hello suggestion" />);

    await waitFor(() => {
      expect((textarea as HTMLTextAreaElement).value).toBe("Hello suggestion");
    });
  });
});

describe("ChatInput — send behavior", () => {
  it("should call onSend with the message when the send button is clicked", async () => {
    const onSend = vi.fn();
    render(<ChatInput onSend={onSend} />);

    const textarea = screen.getByLabelText(/message input/i);
    fireEvent.change(textarea, { target: { value: "Hello world" } });

    const sendButton = screen.getByLabelText(/send message/i);
    fireEvent.click(sendButton);

    await waitFor(() => {
      expect(onSend).toHaveBeenCalledWith("Hello world");
    });
  });

  it("should clear the textarea after sending", async () => {
    const onSend = vi.fn();
    render(<ChatInput onSend={onSend} />);

    const textarea = screen.getByLabelText(/message input/i);
    fireEvent.change(textarea, { target: { value: "Hello world" } });
    fireEvent.click(screen.getByLabelText(/send message/i));

    await waitFor(() => {
      expect((textarea as HTMLTextAreaElement).value).toBe("");
    });
  });

  it("should show the stop button when streaming", () => {
    render(<ChatInput onSend={vi.fn()} isStreaming={true} />);
    expect(screen.getByLabelText(/stop generating/i)).toBeDefined();
  });

  it("should call onStopStreaming when stop button is clicked", () => {
    const onStop = vi.fn();
    render(<ChatInput onSend={vi.fn()} isStreaming={true} onStopStreaming={onStop} />);
    fireEvent.click(screen.getByLabelText(/stop generating/i));
    expect(onStop).toHaveBeenCalledOnce();
  });
});

describe("ChatInput — command autocomplete", () => {
  it("shows no autocomplete for regular text", () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "hello world" } });
    });

    expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
  });

  it("shows autocomplete dropdown when user types /", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });
  });

  it("shows all commands when only / is typed", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      const dropdown = screen.getByTestId("command-autocomplete");
      expect(dropdown).toHaveTextContent("/status");
      expect(dropdown).toHaveTextContent("/agents");
      expect(dropdown).toHaveTextContent("/jobs");
      expect(dropdown).toHaveTextContent("/pause");
      expect(dropdown).toHaveTextContent("/resume");
      expect(dropdown).toHaveTextContent("/help");
    });
  });

  it("filters commands by typed prefix", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/ag" } });
    });

    await waitFor(() => {
      const dropdown = screen.getByTestId("command-autocomplete");
      expect(dropdown).toHaveTextContent("/agents");
      expect(dropdown).not.toHaveTextContent("/status");
      expect(dropdown).not.toHaveTextContent("/pause");
    });
  });

  it("dismisses autocomplete on Escape key", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });

    act(() => {
      fireEvent.keyDown(textarea, { key: "Escape" });
    });

    await waitFor(() => {
      expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
    });
  });

  it("accepts first command on Tab key", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/stat" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });

    act(() => {
      fireEvent.keyDown(textarea, { key: "Tab" });
    });

    await waitFor(() => {
      expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
    });

    expect((textarea as HTMLTextAreaElement).value).toBe("/status ");
  });

  it("navigates with ArrowDown key", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });

    act(() => {
      fireEvent.keyDown(textarea, { key: "ArrowDown" });
    });

    await waitFor(() => {
      const options = screen.getAllByRole("option");
      // Second item should be selected after ArrowDown
      expect(options[1]).toHaveAttribute("aria-selected", "true");
    });
  });

  it("fills command when clicking a suggestion", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });

    // Click on /agents option
    const options = screen.getAllByRole("option");
    const agentsOption = options.find((o) => o.textContent.includes("/agents"));
    if (!agentsOption) throw new Error("Could not find /agents option");

    act(() => {
      fireEvent.click(agentsOption);
    });

    await waitFor(() => {
      expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
    });

    expect((textarea as HTMLTextAreaElement).value).toBe("/agents ");
  });

  it("shows command descriptions", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      const dropdown = screen.getByTestId("command-autocomplete");
      expect(dropdown).toHaveTextContent("Show orchestrator health");
      expect(dropdown).toHaveTextContent("Pause the job queue");
    });
  });

  it("hides autocomplete when input no longer starts with /", async () => {
    render(<ChatInput onSend={vi.fn()} />);
    const textarea = screen.getByLabelText(/message input/i);

    act(() => {
      fireEvent.change(textarea, { target: { value: "/" } });
    });

    await waitFor(() => {
      expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
    });

    act(() => {
      fireEvent.change(textarea, { target: { value: "" } });
    });

    await waitFor(() => {
      expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
    });
  });
});
