// #region TestAgentChat.Model.EdgeCases [C:2] [TYPE Module] [SEMANTICS test,model,agent-chat,edge,coverage]
// @BRIEF Supplementary edge case tests for AgentChatModel — loadConversations nullish coalescing, stream parsing fallbacks, error paths.
// @RELATION BINDS_TO -> [AgentChat.Model]
// @TEST_EDGE: missing_conversation_id -> loadConversations falls back to item.id then ""
// @TEST_EDGE: nullish_fields -> loadConversations uses ?? fallbacks for title/updated_at/message_count
// @TEST_EDGE: disconnected_send -> sendMessage returns early when not connected
import { describe, it, expect, vi, beforeEach } from "vitest";

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

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 }),
}));

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

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

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

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

describe("AgentChatModel — loadConversations nullish coalescing", () => {
  let model: AgentChatModel;

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

  it("uses item.id fallback when conversation_id and id are missing", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockResolvedValue({
      items: [{ title: "No Id", updated_at: "", message_count: 0 }],
      has_next: false,
    });
    await model.loadConversations(true);
    expect(model.conversations).toHaveLength(1);
    expect(model.conversations[0].id).toBe(""); // conversation_id ?? id ?? "" → undefined ?? undefined ?? "" → ""
  });

  it("uses id fallback when conversation_id is missing", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockResolvedValue({
      items: [{ id: "item-id", title: "ById", updated_at: "", message_count: 0 }],
      has_next: false,
    });
    await model.loadConversations(true);
    expect(model.conversations[0].id).toBe("item-id");
  });

  it("uses nullish fallbacks for title, updated_at, message_count", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockResolvedValue({
      items: [{ conversation_id: "c1" }], // no title, updated_at, message_count
      has_next: false,
    });
    await model.loadConversations(true);
    const conv = model.conversations[0];
    expect(conv.title).toBe("");
    expect(conv.updated_at).toBe("");
    expect(conv.message_count).toBe(0);
  });

  it("appends conversations when reset=false with nullish fields", 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" }],
      has_next: false,
    });
    await model.loadConversations(false);
    expect(model.conversations).toHaveLength(2);
    expect(model.conversations[1].id).toBe("c1");
  });

  it("loadConversations handles error in catch with non-Error", async () => {
    const { getAssistantConversations } = await import("$lib/api/assistant.js");
    vi.mocked(getAssistantConversations).mockRejectedValue("string error");
    await model.loadConversations(true);
    expect(model.error).toBe("Failed to load conversations");
  });
});

describe("AgentChatModel — loadHistory edge cases", () => {
  let model: AgentChatModel;

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

  it("returns early when conversationId is null and no currentConversationId", async () => {
    model.currentConversationId = null;
    await model.loadHistory(null);
    expect(model.isLoadingHistory).toBe(false);
  });

  it("uses nullish fallbacks for message fields", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:missing-fields");
    vi.mocked(getAssistantHistory).mockResolvedValue({
      items: [{ message_id: "m1" }], // minimal fields
      conversation_id: "missing-fields",
    });
    model.currentConversationId = null;
    await model.loadHistory("missing-fields");
    expect(model.messages).toHaveLength(1);
    expect(model.messages[0].role).toBe("assistant");
    expect(model.messages[0].text).toBe("");
    expect(model.messages[0].toolCalls).toEqual([]);
  });

  it("uses msg.id fallback when message_id is missing", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:id-fallback");
    vi.mocked(getAssistantHistory).mockResolvedValue({
      items: [{ id: "fallback-id", role: "user", text: "hi", conversation_id: "id-fallback", created_at: "" }],
      conversation_id: "id-fallback",
    });
    model.currentConversationId = null;
    await model.loadHistory("id-fallback");
    expect(model.messages[0].id).toBe("fallback-id");
  });

  it("handles loadHistory catch with non-Error", async () => {
    const { getAssistantHistory } = await import("$lib/api/assistant.js");
    localStorage.removeItem("agentchat:messages:catch-test");
    vi.mocked(getAssistantHistory).mockRejectedValue("string error");
    model.currentConversationId = "catch-test";
    await model.loadHistory();
    expect(model.error).toBe("Failed to load history");
    expect(model.isLoadingHistory).toBe(false);
  });

  it("deleteConversation handles non-Error in catch", async () => {
    const { deleteAssistantConversation } = await import("$lib/api/assistant.js");
    vi.mocked(deleteAssistantConversation).mockRejectedValue("string error");
    model.conversations = [{ id: "c1", title: "Chat", updated_at: "", message_count: 0 }];
    model.currentConversationId = "c1";
    await model.deleteConversation("c1");
    expect(model.error).toBe("Failed to archive conversation");
    // rolled back
    expect(model.conversations).toHaveLength(1);
  });

  it("createConversation does not save when no currentConversationId", () => {
    model.currentConversationId = null;
    const saveSpy = vi.spyOn(model["storage"], "save");
    model.createConversation();
    expect(saveSpy).not.toHaveBeenCalled();
    saveSpy.mockRestore();
  });
});

describe("AgentChatModel — sendMessage edge cases", () => {
  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",
    });
  });

  it("sendMessage with whitespace-only text returns early", async () => {
    const prev = model._submission;
    await model.sendMessage("   ");
    expect(model._submission).toBe(prev);
  });

  it("sendMessage with files but no text proceeds (submit called)", async () => {
    const file = new File(["test"], "test.txt");
    model._client!.submit = vi.fn().mockReturnValue({
      [Symbol.asyncIterator]: () => ({ next: vi.fn().mockResolvedValue({ done: true }) }),
      cancel: vi.fn(),
    });
    await model.sendMessage("", [file]).catch(() => {});
    // submit should have been called — empty text with files passes the guard
    expect(model._client!.submit).toHaveBeenCalled();
    // After stream completes, streamingState returns to idle
    expect(model.streamingState).toBe("idle");
  });
});

describe("AgentChatModel — localStorage edge cases", () => {
  let model: AgentChatModel;

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

  it("_loadFromLocalStorage returns null on JSON parse error", () => {
    localStorage.setItem("agentchat:messages:bad-json", "not json");
    const result = model["storage"]["load"]("bad-json");
    expect(result).toBeNull();
  });

  it("_clearLocalStorage handles missing key gracefully", () => {
    expect(() => model["storage"]["clear"]("nonexistent")).not.toThrow();
  });

  it("_persistMessages does nothing without conversationId", () => {
    model.currentConversationId = null;
    expect(() => model["_persistMessages"]()).not.toThrow();
  });
});

describe("AgentChatModel — cancelGeneration edge cases", () => {
  it("cancelGeneration with null _submission", () => {
    const model = new AgentChatModel();
    model.streamingState = "streaming";
    model._submission = null;
    expect(() => model.cancelGeneration()).not.toThrow();
    expect(model.streamingState).toBe("idle");
  });
});

describe("AgentChatModel — _processStream complex formats", () => {
  let model: AgentChatModel;

  beforeEach(() => {
    model = new AgentChatModel();
  });

  it("handles array with object element (no text field)", async () => {
    const events = [
      { type: "data", data: [{ content: null }] },
    ];
    let i = 0;
    const submission = { [Symbol.asyncIterator]: () => ({ next: () => i < events.length ? Promise.resolve({ value: events[i++], done: false }) : Promise.resolve({ done: true }) }) };
    await model["streamProcessor"]["processStream"](submission as any, "conv-1");
    // No crash, empty text handled
  });

  it("handles object with text field fallback", async () => {
    const events = [
      { type: "data", data: { text: "plain text" } },
    ];
    let i = 0;
    const submission = { [Symbol.asyncIterator]: () => ({ next: () => i < events.length ? Promise.resolve({ value: events[i++], done: false }) : Promise.resolve({ done: true }) }) };
    await model["streamProcessor"]["processStream"](submission as any, "conv-1");
    expect(model.partialText).toBe("plain text");
  });

  it("handles JSON string with metadata in content field", async () => {
    const events = [
      { type: "data", data: ['{"content":"parsed content","metadata":{"type":"stream_token","token":"token"}}'] },
    ];
    let i = 0;
    const submission = { [Symbol.asyncIterator]: () => ({ next: () => i < events.length ? Promise.resolve({ value: events[i++], done: false }) : Promise.resolve({ done: true }) }) };
    await model["streamProcessor"]["processStream"](submission as any, "conv-1");
    expect(model.partialTokens).toContain("token");
  });

  it("handles sendMessage when queue processing is in progress", async () => {
    // This tests the _processingQueue guard in _sendNow
    Object.assign(model, {
      _client: { submit: vi.fn() },
      connectionState: "connected",
    });
    (model as any)._processingQueue = true;
    await (model as any)._sendNow("test");
    expect(model._client!.submit).not.toHaveBeenCalled();
  });
});
// #endregion TestAgentChat.Model.EdgeCases
