// #region TestAgentChat.Model [C:2] [TYPE Module] [SEMANTICS test,model,agent-chat]
// @BRIEF L1 model tests for AgentChatModel — no DOM render, pure state machine verification.
// @RELATION BINDS_TO -> [AgentChat.Model]
// @INVARIANT sendMessage transitions idle->streaming, cancelGeneration resets to idle.
// @INVARIANT resumeConfirm transitions awaiting_confirmation->idle on deny, streaming on confirm.
// @INVARIANT createConversation resets all state.
import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock @gradio/client before importing model
vi.mock("@gradio/client", () => ({
  Client: {
    connect: vi.fn().mockResolvedValue({
      submit: vi.fn().mockReturnValue({
        [Symbol.asyncIterator]: () => ({
          next: vi.fn().mockResolvedValue({ done: true, value: undefined }),
        }),
        cancel: vi.fn(),
      }),
    }),
  },
}));

// Mock $lib/api/assistant.js
vi.mock("$lib/api/assistant.js", () => ({
  getAssistantConversations: vi.fn().mockResolvedValue({ items: [], has_next: false }),
  getAssistantHistory: vi.fn().mockResolvedValue({ items: [], has_next: false }),
  deleteAssistantConversation: vi.fn().mockResolvedValue({ deleted: true }),
}));

// Mock $lib/stores/assistantChat.svelte.js
vi.mock("$lib/stores/assistantChat.svelte.js", () => ({
  assistantChatStore: { value: { isOpen: false, conversationId: null } },
  setAssistantConversationId: vi.fn(),
}));

// Mock $lib/toasts.svelte.js
vi.mock("$lib/toasts.svelte.js", () => ({
  addToast: vi.fn(),
}));

// Mock $lib/cot-logger
vi.mock("$lib/cot-logger", () => ({
  log: vi.fn(),
}));

import { AgentChatModel } from "../AgentChatModel.svelte.ts";

// #region TestAgentChat.Model.StateMachine [C:2] [TYPE Function] [SEMANTICS test,model,state]
// @BRIEF State transition tests: idle->streaming, streaming->idle, awaiting_confirmation->idle.
// @TEST_EDGE send_message_valid, cancel_generation, resume_confirm, resume_deny
describe("AgentChatModel — State Machine", () => {
  let model: AgentChatModel;

  beforeEach(() => {
    model = new AgentChatModel();
    Object.assign(model, {
      _client: { submit: vi.fn().mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: vi.fn().mockResolvedValue({ done: true }) }), cancel: vi.fn() }) },
      connectionState: "connected",
    });
  });

  // #region TestAgentChat.Model.SendMessageTransition [C:2] [TYPE Test]
  it("sendMessage transitions idle -> streaming", async () => {
    expect(model.streamingState).toBe("idle");
    const sendPromise = model.sendMessage("hello");
    expect(model.streamingState).toBe("streaming");
    await sendPromise.catch(() => {});
  });
  // #endregion

  // #region TestAgentChat.Model.SendMessageGuardEmpty [C:2] [TYPE Test]
  it("sendMessage returns early on empty text", async () => {
    const submissionBefore = model._submission;
    await model.sendMessage("");
    expect(model._submission).toBe(submissionBefore);
  });
  // #endregion

  // #region TestAgentChat.Model.SendMessageGuardNotConnected [C:2] [TYPE Test]
  it("sendMessage returns early when not connected", async () => {
    model.connectionState = "disconnected";
    await model.sendMessage("hello");
    expect(model.streamingState).toBe("idle");
  });
  // #endregion

  // #region TestAgentChat.Model.SendMessageQueue [C:2] [TYPE Test]
  it("sendMessage enqueues when already streaming", async () => {
    model.streamingState = "streaming";
    expect(model["_messageQueue"].length).toBe(0);
    await model.sendMessage("queued_msg");
    expect(model["_messageQueue"].length).toBe(1);
    expect(model["_messageQueue"][0].text).toBe("queued_msg");
  });
  // #endregion

  // #region TestAgentChat.Model.CancelGeneration [C:2] [TYPE Test]
  it("cancelGeneration resets streaming -> idle", () => {
    model.streamingState = "streaming";
    model.cancelGeneration();
    expect(model.streamingState).toBe("idle");
    expect(model._submission).toBeNull();
  });

  it("cancelGeneration on idle is no-op", () => {
    model.streamingState = "idle";
    model.cancelGeneration();
    expect(model.streamingState).toBe("idle");
  });

  it("cancelGeneration calls submission.cancel()", () => {
    const cancelFn = vi.fn();
    model._submission = { cancel: cancelFn } as any;
    model.streamingState = "streaming";
    model.cancelGeneration();
    expect(cancelFn).toHaveBeenCalled();
  });
  // #endregion

  // #region TestAgentChat.Model.ResumeConfirm [C:2] [TYPE Test]
  it("resumeConfirm with confirm transitions awaiting_confirmation -> idle after stream", async () => {
    model.streamingState = "awaiting_confirmation";
    model.currentConversationId = "test-conv";
    model.pendingThreadId = "test-thread";
    const promise = model.resumeConfirm("confirm");
    await promise.catch(() => {});
  });

  it("resumeConfirm with deny transitions awaiting_confirmation -> idle after stream", async () => {
    model.streamingState = "awaiting_confirmation";
    model.currentConversationId = "test-conv";
    model.pendingThreadId = "test-thread";
    const promise = model.resumeConfirm("deny");
    await promise.catch(() => {});
  });

  it("resumeConfirm on idle state is no-op", async () => {
    model.streamingState = "idle";
    await model.resumeConfirm("confirm");
  });

  it("resumeConfirm returns early when pendingThreadId is null", async () => {
    model.streamingState = "awaiting_confirmation";
    model.pendingThreadId = null;
    const submitBefore = model._client?.submit;
    await model.resumeConfirm("confirm");
    expect(model.streamingState).toBe("awaiting_confirmation");
  });
  // #endregion

  // #region TestAgentChat.Model.CreateConversation [C:2] [TYPE Test]
  it("createConversation resets all state", () => {
    model.messages = [{ id: "1", conversation_id: "c1", role: "user", text: "hi", toolCalls: [], created_at: "" }];
    model.currentConversationId = "c1";
    model.streamingState = "streaming";
    model.error = "some error";
    model.partialText = "partial";
    model.activeToolCalls = [{ tool: "test", input: {}, status: "executing" }];
    model.pendingThreadId = "thread-1";

    model.createConversation();

    expect(model.messages).toEqual([]);
    expect(model.currentConversationId).toBeNull();
    expect(model.streamingState).toBe("idle");
    expect(model.error).toBeNull();
    expect(model.partialText).toBe("");
    expect(model.activeToolCalls).toEqual([]);
    expect(model.pendingThreadId).toBeNull();
  });
  // #endregion
});
// #endregion

// #region TestAgentChat.Model.DerivedState [C:2] [TYPE Function]
// @BRIEF Derived state correctness tests.
describe("AgentChatModel — Derived State", () => {
  let model: AgentChatModel;
  beforeEach(() => { model = new AgentChatModel(); });

  it("isStreaming is true when streaming", () => {
    model.streamingState = "streaming";
    expect(model.isStreaming).toBe(true);
  });

  it("isStreaming is true when awaiting_confirmation", () => {
    model.streamingState = "awaiting_confirmation";
    expect(model.isStreaming).toBe(true);
  });

  it("isStreaming is false when idle", () => {
    model.streamingState = "idle";
    expect(model.isStreaming).toBe(false);
  });

  it("isInputLocked is true during streaming", () => {
    model.streamingState = "streaming";
    expect(model.isInputLocked).toBe(true);
  });

  it("isInputLocked is true when disconnected", () => {
    model.streamingState = "disconnected";
    expect(model.isInputLocked).toBe(true);
  });

  it("isInputLocked is false when idle", () => {
    model.streamingState = "idle";
    expect(model.isInputLocked).toBe(false);
  });

  it("connectionDotColor returns success when connected", () => {
    model.connectionState = "connected";
    expect(model.connectionDotColor).toBe("success");
  });

  it("connectionDotColor returns warning when disconnected", () => {
    model.connectionState = "disconnected";
    expect(model.connectionDotColor).toBe("warning");
  });

  it("connectionDotColor returns destructive when disconnected_permanent", () => {
    model.connectionState = "disconnected_permanent";
    expect(model.connectionDotColor).toBe("destructive");
  });

  it("queuePosition returns message queue length", () => {
    model["_messageQueue"] = [{ text: "a" }, { text: "b" }] as any;
    expect(model.queuePosition).toBe(2);
  });
});
// #endregion

// #region TestAgentChat.Model.MetadataHandling [C:2] [TYPE Function]
// @BRIEF Metadata dispatch tests: tool_start, tool_end, error -> error state.
describe("AgentChatModel — Metadata Handling", () => {
  let model: AgentChatModel;
  beforeEach(() => { model = new AgentChatModel(); });

  it("handles stream_token metadata", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "stream_token", token: "Hello" } });
    expect(model.partialText).toBe("Hello");
    model._onStreamData({ id: "2", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "stream_token", token: " world" } });
    expect(model.partialText).toBe("Hello world");
  });

  it("skips stream_token dedup when token already at end", () => {
    model.partialText = "Hello world";
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "stream_token", token: "world" } });
    expect(model.partialText).toBe("Hello world");
  });

  it("handles tool_start metadata", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "tool_start", tool: "search_dashboards", input: { query: "test" } } });
    expect(model.activeToolCalls).toHaveLength(1);
    expect(model.activeToolCalls[0].tool).toBe("search_dashboards");
    expect(model.activeToolCalls[0].status).toBe("executing");
  });

  it("handles tool_end metadata", () => {
    model.activeToolCalls = [{ tool: "search_dashboards", input: {}, status: "executing" }];
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "tool_end", tool: "search_dashboards", output: { result: "ok" } } });
    expect(model.activeToolCalls[0].status).toBe("completed");
    expect(model.activeToolCalls[0].output).toEqual({ result: "ok" });
  });

  it("handles tool_error metadata", () => {
    model.activeToolCalls = [{ tool: "search_dashboards", input: {}, status: "executing" }];
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "tool_error", tool: "search_dashboards", error: "API timeout" } });
    expect(model.activeToolCalls[0].status).toBe("failed");
    expect(model.activeToolCalls[0].error).toBe("API timeout");
  });

  it("handles confirm_required metadata", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "confirm_required", prompt: "Deploy?", thread_id: "thread-1" } });
    expect(model.streamingState).toBe("awaiting_confirmation");
    expect(model.pendingThreadId).toBe("thread-1");
  });

  it("handles confirm_resolved metadata", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "confirm_resolved", result: "confirmed" } });
    expect(model.streamingState).toBe("streaming");
    expect(model.pendingThreadId).toBeNull();
  });

  it("handles confirm_resolved with denied result", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "confirm_resolved", result: "denied" } });
    expect(model.streamingState).toBe("idle");
  });

  it("handles error metadata", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "error", code: "LLM_UNAVAILABLE", detail: "LLM not responding" } });
    expect(model.streamingState).toBe("error");
    expect(model.error).toBe("LLM not responding");
  });

  it("handles plain text (no metadata)", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "plain text reply", toolCalls: [], created_at: "" });
    expect(model.partialText).toBe("plain text reply");
  });

  it("handles unknown metadata type", () => {
    model._onStreamData({ id: "1", conversation_id: "c1", role: "assistant", text: "", toolCalls: [], created_at: "",
      metadata: { type: "some_unknown_type" as any } });
    // No crash, log called
    expect(model.streamingState).toBe("idle");
  });
});
// #endregion

// #region TestAgentChat.Model.DataLoading [C:2] [TYPE Function]
// @BRIEF Data loading actions: loadConversations, loadHistory, deleteConversation, retryConnection.
describe("AgentChatModel — Data Loading & Actions", () => {
  let model: AgentChatModel;
  beforeEach(() => {
    model = new AgentChatModel();
    Object.assign(model, {
      _client: { submit: vi.fn().mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: vi.fn().mockResolvedValue({ done: true }) }), cancel: vi.fn() }) },
      connectionState: "connected",
    });
  });

  // #region TestAgentChat.Model.LoadConversations [C:2] [TYPE Test]
  it("loadConversations fetches and maps items (reset=true)", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockResolvedValue({
      items: [{ conversation_id: "c1", title: "Chat 1", updated_at: "2026-01-01", message_count: 5 }],
      has_next: false,
    });
    await model.loadConversations(true);
    expect(model.conversations).toHaveLength(1);
    expect(model.conversations[0].id).toBe("c1");
    expect(model.conversations[0].title).toBe("Chat 1");
  });

  it("loadConversations appends items when reset=false", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    model.conversations = [{ id: "c0", title: "Old", updated_at: "", message_count: 0 }];
    vi.mocked(getAssistantConversations).mockResolvedValue({
      items: [{ conversation_id: "c1", title: "Chat 1", updated_at: "2026-01-01", message_count: 5 }],
      has_next: false,
    });
    await model.loadConversations(false);
    expect(model.conversations).toHaveLength(2);
  });

  it("loadConversations handles error gracefully", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockRejectedValue(new Error("Network error"));
    await model.loadConversations(true);
    expect(model.error).toBe("Network error");
  });
  // #endregion

  // #region TestAgentChat.Model.LoadHistory [C:2] [TYPE Test]
  it("loadHistory sets isLoading and clears error", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:load-c1");
    vi.mocked(getAssistantHistory).mockResolvedValue({
      items: [{ message_id: "m1", role: "user", text: "hi", conversation_id: "load-c1" }],
      conversation_id: "load-c1",
    });
    model.currentConversationId = "load-c1";
    const promise = model.loadHistory();
    expect(model.isLoadingHistory).toBe(true);
    await promise;
    expect(model.isLoadingHistory).toBe(false);
    expect(model.messages).toHaveLength(1);
  });

  it("loadHistory handles API error", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:load-c2");
    vi.mocked(getAssistantHistory).mockRejectedValue(new Error("History error"));
    model.currentConversationId = "load-c2";
    await model.loadHistory();
    expect(model.error).toBe("History error");
    expect(model.isLoadingHistory).toBe(false);
  });
  // #endregion

  // #region TestAgentChat.Model.DeleteConversation [C:2] [TYPE Test]
  it("deleteConversation optimistically removes then calls API", async () => {
    const { deleteAssistantConversation } = await import("$lib/api/assistant.js");
    vi.mocked(deleteAssistantConversation).mockResolvedValue({ deleted: true });
    model.conversations = [{ id: "c1", title: "Chat", updated_at: "", message_count: 0 }];
    await model.deleteConversation("c1");
    expect(deleteAssistantConversation).toHaveBeenCalledWith("c1");
  });

  it("deleteConversation rolls back on failure", async () => {
    const { deleteAssistantConversation } = await import("$lib/api/assistant.js");
    vi.mocked(deleteAssistantConversation).mockRejectedValue(new Error("API error"));
    model.conversations = [{ id: "c1", title: "Chat", updated_at: "", message_count: 0 }];
    await model.deleteConversation("c1");
    expect(model.conversations).toHaveLength(1);
    expect(model.error).toBe("API error");
  });

  it("deleteConversation calls createConversation when deleting current", async () => {
    const { deleteAssistantConversation } = await import("$lib/api/assistant.js");
    vi.mocked(deleteAssistantConversation).mockResolvedValue({ deleted: true });
    model.currentConversationId = "c1";
    model.conversations = [{ id: "c1", title: "Chat", updated_at: "", message_count: 0 }];
    model.messages = [{ id: "m1", conversation_id: "c1", role: "user", text: "hi", toolCalls: [], created_at: "" }];
    await model.deleteConversation("c1");
    expect(model.currentConversationId).toBeNull();
    expect(model.messages).toEqual([]);
  });
  // #endregion

  // #region TestAgentChat.Model.RetryConnection [C:2] [TYPE Test]
  it("retryConnection succeeds and sets state to connected", async () => {
    const { Client } = await import("@gradio/client");
    vi.mocked(Client.connect).mockResolvedValue({ submit: vi.fn() } as any);
    await model.retryConnection();
    expect(model.connectionState).toBe("connected");
    expect(model.streamingState).toBe("idle");
  });

  it("retryConnection sets disconnected_permanent on failure", async () => {
    const { Client } = await import("@gradio/client");
    vi.mocked(Client.connect).mockRejectedValue(new Error("Gradio down"));
    await model.retryConnection();
    expect(model.connectionState).toBe("disconnected_permanent");
  });
  // #endregion
});
// #endregion

// #region TestAgentChat.Model.ConnectionLifecycle [C:2] [TYPE Function]
// @BRIEF Connection lifecycle: disconnect triggers reconnect loop, max attempts -> permanent.
describe("AgentChatModel — Connection Lifecycle", () => {
  let model: AgentChatModel;
  beforeEach(() => { model = new AgentChatModel(); });

  it("onDisconnect sets state and starts reconnect loop", () => {
    model.connectionState = "connected";
    model["_startReconnectLoop"] = vi.fn();
    model["_onDisconnect"]();
    expect(model.connectionState).toBe("disconnected");
  });

  it("onReconnect restores connected state", () => {
    model.connectionState = "disconnected";
    model.streamingState = "error";
    model["_onReconnect"]();
    expect(model.connectionState).toBe("connected");
    expect(model.streamingState).toBe("idle");
  });

  it("_startReconnectLoop does not start if timer exists", () => {
    model["_reconnectTimer"] = 123 as any;
    model["_startReconnectLoop"]();
    // No crash, timer not replaced
  });
});
// #endregion

// #region TestAgentChat.Model.CaptureConversationId [C:2] [TYPE Test]
// @BRIEF Private helper: _captureConversationId captures thread_id from metadata.
describe("AgentChatModel — captureConversationId", () => {
  let model: AgentChatModel;
  beforeEach(() => { model = new AgentChatModel(); });

  it("sets currentConversationId from thread_id in metadata", () => {
    model["_captureConversationId"]({ thread_id: "thread-1" }, "");
    expect(model.currentConversationId).toBe("thread-1");
  });

  it("does not override existing conversationId", () => {
    model.currentConversationId = "existing-id";
    model["_captureConversationId"]({ thread_id: "new-thread" }, "");
    expect(model.currentConversationId).toBe("existing-id");
  });

  it("does nothing when thread_id is falsy and convId is empty", () => {
    model["_captureConversationId"](undefined, "");
    expect(model.currentConversationId).toBeNull();
  });

  it("captures conversation_id from event when metadata has no thread_id", () => {
    model["_captureConversationId"](undefined, "conv-from-event");
    expect(model.currentConversationId).toBe("conv-from-event");
  });
});
// #endregion

// #region TestAgentChat.Model.LocalStorage [C:2] [TYPE Function]
// @BRIEF localStorage persistence: save, load, clear operations.
describe("AgentChatModel — localStorage", () => {
  let model: AgentChatModel;
  const STORAGE_KEY = "agentchat:messages:conv-1";

  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    model = new AgentChatModel();
  });

  it("_saveToLocalStorage writes to localStorage", () => {
    model.currentConversationId = "conv-1";
    model.messages = [{ id: "m1", conversation_id: "conv-1", role: "user", text: "hi", toolCalls: [], created_at: "" }];
    model["_saveToLocalStorage"]();
    const stored = localStorage.getItem(STORAGE_KEY);
    expect(stored).not.toBeNull();
    const parsed = JSON.parse(stored!);
    expect(parsed.messages).toHaveLength(1);
    expect(parsed.conversationId).toBe("conv-1");
  });

  it("_loadFromLocalStorage restores messages", () => {
    const data = { messages: [{ id: "m1", role: "user", text: "hi", toolCalls: [], created_at: "" }], conversationId: "conv-1" };
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    const result = model["_loadFromLocalStorage"]("conv-1");
    expect(result).toBe(true);
    expect(model.messages).toHaveLength(1);
    expect(model.currentConversationId).toBe("conv-1");
  });

  it("_loadFromLocalStorage returns false when no data", () => {
    const result = model["_loadFromLocalStorage"]("nonexistent");
    expect(result).toBe(false);
  });

  it("_loadFromLocalStorage returns false when messages is not array", () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ conversationId: "conv-1", messages: "not-array" }));
    const result = model["_loadFromLocalStorage"]("conv-1");
    expect(result).toBe(false);
  });

  it("_clearLocalStorage removes item", () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ messages: [], conversationId: "conv-1" }));
    model["_clearLocalStorage"]("conv-1");
    expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
  });

  it("_persistMessages calls _saveToLocalStorage when conversationId exists", () => {
    const saveSpy = vi.spyOn(model as any, "_saveToLocalStorage").mockImplementation(() => {});
    model.currentConversationId = "conv-1";
    model["_persistMessages"]();
    expect(saveSpy).toHaveBeenCalled();
    saveSpy.mockRestore();
  });

  it("deleteConversation calls _clearLocalStorage", async () => {
    const { deleteAssistantConversation } = await import("$lib/api/assistant.js");
    vi.mocked(deleteAssistantConversation).mockResolvedValue({ deleted: true });
    model.conversations = [{ id: "c1", title: "Chat", updated_at: "", message_count: 0 }];
    model.currentConversationId = "c1";
    const clearSpy = vi.spyOn(model as any, "_clearLocalStorage");
    await model.deleteConversation("c1");
    expect(clearSpy).toHaveBeenCalledWith("c1");
    clearSpy.mockRestore();
  });

  it("createConversation saves to localStorage when messages exist", () => {
    const saveSpy = vi.spyOn(model as any, "_saveToLocalStorage").mockImplementation(() => {});
    model.currentConversationId = "c1";
    model.messages = [{ id: "m1", conversation_id: "c1", role: "user", text: "hi", toolCalls: [], created_at: "" }];
    model.createConversation();
    expect(saveSpy).toHaveBeenCalled();
    saveSpy.mockRestore();
    expect(model.messages).toEqual([]);
  });

  it("_saveToLocalStorage silently ignores localStorage errors", () => {
    const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { throw new Error("QuotaExceeded"); });
    model.currentConversationId = "conv-1";
    expect(() => model["_saveToLocalStorage"]()).not.toThrow();
    setItemSpy.mockRestore();
  });

  it("_loadFromLocalStorage silently ignores parse errors", () => {
    localStorage.setItem(STORAGE_KEY, "invalid-json");
    const result = model["_loadFromLocalStorage"]("conv-1");
    expect(result).toBe(false);
  });
});

// #region TestAgentChat.Model.AdvancedPaths [C:2] [TYPE Function]
// @BRIEF Edge cases: error paths, stream parsing, reconnect lifecycle.
describe("AgentChatModel — Advanced Paths", () => {
  let model: AgentChatModel;
  beforeEach(() => {
    model = new AgentChatModel();
    Object.assign(model, {
      _client: { submit: vi.fn(), stream_status: {} },
      connectionState: "connected",
    });
  });

  it("_sendNow catch block on submit error", async () => {
    model._client!.submit = vi.fn().mockReturnValue({
      [Symbol.asyncIterator]: () => ({ next: vi.fn().mockRejectedValue(new Error("Stream error")) }),
    });
    await model["_sendNow"]("hello");
    expect(model.streamingState).toBe("error");
    expect(model.error).toBe("Stream error");
  });

  it("_processStream heartbeat + array + object formats", async () => {
    const events = [
      { type: "heartbeat", data: "" },
      { type: "data", data: ["plain string"] },
      { type: "data", data: [{ content: "obj", metadata: { type: "tool_start", tool: "t1" } }] },
      { type: "data", data: { content: "raw", metadata: { type: "stream_token", token: "raw" } } },
    ];
    let i = 0;
    const submission = { [Symbol.asyncIterator]: () => ({ next: () => i < events.length ? Promise.resolve({ value: events[i++], done: false }) : Promise.resolve({ done: true }) }) };
    await model["_processStream"](submission as any, "conv-1");
    expect(model.activeToolCalls).toHaveLength(1);
  });

  it("_processStream array non-string + JSON parse error", async () => {
    const events = [
      { type: "data", data: [{ notext: true }] },
      { type: "data", data: ["{invalid json}"] },
      { type: "data", data: "not json" },
      { type: "data", data: '{"content":"parsed"}' },
    ];
    let i = 0;
    const submission = { [Symbol.asyncIterator]: () => ({ next: () => i < events.length ? Promise.resolve({ value: events[i++], done: false }) : Promise.resolve({ done: true }) }) };
    await model["_processStream"](submission as any, null);
    expect(model.partialText.length).toBeGreaterThan(0);
  });

  it("_onReconnect clears timer", () => {
    model["_reconnectTimer"] = 123 as any;
    const clearSpy = vi.spyOn(globalThis, "clearInterval");
    model["_onReconnect"]();
    expect(clearSpy).toHaveBeenCalled();
    expect(model["_reconnectTimer"]).toBeNull();
    clearSpy.mockRestore();
  });

  it("_processingQueue guard + _drainQueue", async () => {
    model["_processingQueue"] = true;
    const submitSpy = vi.spyOn(model._client!, "submit");
    await model["_sendNow"]("test");
    expect(submitSpy).not.toHaveBeenCalled();
    model["_processingQueue"] = false;
    model["_messageQueue"] = [{ text: "q1" }];
    model._client!.submit = vi.fn().mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: vi.fn().mockResolvedValue({ done: true }) }) });
    const sendSpy = vi.spyOn(model as any, "_sendNow");
    await model["_drainQueue"]();
    expect(sendSpy).toHaveBeenCalled();
    sendSpy.mockRestore();
  });

  it("_drainQueue processing guard on re-entry", async () => {
    model["_processingQueue"] = true;
    const drainSpy = vi.spyOn(model as any, "_drainQueue");
    // This should just return early at line 206
    await model["_drainQueue"]();
    expect(model["_processingQueue"]).toBe(true);
    drainSpy.mockRestore();
  });

  it("_sendNow streamDone false path", async () => {
    const processSpy = vi.spyOn(model as any, "_processStream").mockReturnValue(new Promise(() => {}));
    const watcherSpy = vi.spyOn(model as any, "_streamCloseWatcher").mockResolvedValue(false);
    model._client!.submit = vi.fn().mockReturnValue({
      [Symbol.asyncIterator]: () => ({ next: vi.fn(), cancel: vi.fn(), return: vi.fn() }),
      cancel: vi.fn(), return: vi.fn(),
    });
    model._client!.stream_status = { open: true };
    await model["_sendNow"]("hello");
    expect(model.streamingState).toBe("idle");
    processSpy.mockRestore();
    watcherSpy.mockRestore();
  });

  it("_streamCloseWatcher with open cycle", async () => {
    model._client!.stream_status = { open: true };
    const watcherPromise = model["_streamCloseWatcher"](500);
    await new Promise(r => setTimeout(r, 150));
    model._client!.stream_status = { open: false };
    const result = await watcherPromise;
    expect(result).toBe(false);
  });

  it("loadHistory guard + localStorage path", async () => {
    model.currentConversationId = null;
    await model.loadHistory();
    expect(model.isLoadingHistory).toBe(false);
    localStorage.setItem("agentchat:messages:ls-conv", JSON.stringify({ messages: [{ id: "m1", role: "user", text: "hi", toolCalls: [], created_at: "" }], conversationId: "ls-conv" }));
    model.currentConversationId = "ls-conv";
    await model.loadHistory();
    expect(model.messages).toHaveLength(1);
    localStorage.removeItem("agentchat:messages:ls-conv");
  });

  it("loadHistory sets conversationId from API", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:new-c");
    vi.mocked(getAssistantHistory).mockResolvedValue({ items: [{ message_id: "m1", role: "user", text: "hi", conversation_id: "api-c" }], conversation_id: "api-c" });
    model.currentConversationId = null;
    await model.loadHistory("new-c");
    expect(model.currentConversationId).toBe("api-c");
  });

  it("resumeConfirm iterates + catch", async () => {
    model.streamingState = "awaiting_confirmation";
    model.currentConversationId = "conv-1";
    model.pendingThreadId = "thread-1";
    let calls = 0;
    model._client = { submit: vi.fn().mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: () => { calls++; return calls === 1 ? Promise.resolve({ value: { type: "data", data: { text: "ok" } }, done: false }) : Promise.resolve({ done: true }); } }) }) } as any;
    await model.resumeConfirm("confirm");
    expect(model.streamingState).toBe("idle");
    model._client = { submit: vi.fn().mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: vi.fn().mockRejectedValue(new Error("Resume error")) }) }) } as any;
    model.streamingState = "awaiting_confirmation";
    model.pendingThreadId = "thread-1";
    await model.resumeConfirm("confirm");
    expect(model.streamingState).toBe("error");
  });

  it("_startReconnectLoop max attempts", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    const { Client } = await import("@gradio/client");
    vi.mocked(Client.connect).mockRejectedValue(new Error("Fail"));
    model["_startReconnectLoop"]();
    for (let i = 0; i < 6; i++) {
      await vi.advanceTimersByTimeAsync(5000);
    }
    expect(model.connectionState).toBe("disconnected_permanent");
    vi.useRealTimers();
  });

  it("_streamCloseWatcher returns false at deadline", async () => {
    const result = await model["_streamCloseWatcher"](0);
    expect(result).toBe(false);
  });
});
// #endregion
// #endregion
