import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { get } from "svelte/store";
import { sessionsStore } from "$lib/stores/sessions";
import { setMockInvokeResult } from "../../../vitest.setup";
import type { SavedSession } from "$lib/stores/sessions";

vi.mock("@tauri-apps/plugin-dialog", () => ({
  save: vi.fn(),
  open: vi.fn(),
}));

vi.mock("@tauri-apps/plugin-fs", () => ({
  writeTextFile: vi.fn().mockResolvedValue(undefined),
  readTextFile: vi.fn(),
}));

vi.mock("@tauri-apps/plugin-opener", () => ({
  openPath: vi.fn().mockResolvedValue(undefined),
}));

const makeSavedSession = (overrides: Partial<SavedSession> = {}): SavedSession => ({
  id: "session-1",
  name: "Test Session",
  created_at: "2026-03-03T10:00:00.000Z",
  last_activity_at: "2026-03-03T11:00:00.000Z",
  working_directory: "/home/naomi/code",
  message_count: 2,
  preview: "Hello world",
  messages: [
    { id: "msg-1", type: "user", content: "Hello world", timestamp: "2026-03-03T10:00:00.000Z" },
    { id: "msg-2", type: "assistant", content: "Hi there!", timestamp: "2026-03-03T10:01:00.000Z" },
  ],
  ...overrides,
});

const makeConversation = () => ({
  id: "conv-1",
  name: "Test Conversation",
  workingDirectory: "/home/naomi/code",
  createdAt: new Date("2026-03-03T10:00:00.000Z"),
  lastActivityAt: new Date("2026-03-03T11:00:00.000Z"),
  terminalLines: [
    {
      id: "line-1",
      type: "user",
      content: "Hello",
      timestamp: new Date("2026-03-03T10:00:00.000Z"),
      toolName: undefined,
    },
    {
      id: "line-2",
      type: "assistant",
      content: "Hi",
      timestamp: new Date("2026-03-03T10:01:00.000Z"),
      toolName: undefined,
    },
  ],
});

describe("sessionsStore - loadSessions", () => {
  it("loads sessions from backend and updates the store", async () => {
    const sessionList = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }];
    setMockInvokeResult("list_sessions", sessionList);
    await sessionsStore.loadSessions();
    expect(get(sessionsStore.sessions)).toEqual(sessionList);
  });

  it("handles errors gracefully", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("list_sessions", new Error("Backend error"));
    await sessionsStore.loadSessions();
    expect(spy).toHaveBeenCalledWith("Failed to load sessions:", expect.any(Error));
    spy.mockRestore();
  });

  it("sets isLoading to false after completion", async () => {
    setMockInvokeResult("list_sessions", []);
    await sessionsStore.loadSessions();
    expect(get(sessionsStore.isLoading)).toBe(false);
  });
});

describe("sessionsStore - loadSession", () => {
  it("returns session data on success", async () => {
    const session = makeSavedSession();
    setMockInvokeResult("load_session", session);
    const result = await sessionsStore.loadSession("session-1");
    expect(result).toEqual(session);
  });

  it("returns null on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", new Error("Not found"));
    const result = await sessionsStore.loadSession("session-1");
    expect(result).toBeNull();
    spy.mockRestore();
  });
});

describe("sessionsStore - deleteSession", () => {
  it("deletes session and reloads the session list", async () => {
    setMockInvokeResult("delete_session", undefined);
    setMockInvokeResult("list_sessions", []);
    await sessionsStore.deleteSession("session-1");
    expect(get(sessionsStore.sessions)).toEqual([]);
  });

  it("handles errors gracefully", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("delete_session", new Error("Failed"));
    await sessionsStore.deleteSession("session-1");
    expect(spy).toHaveBeenCalledWith("Failed to delete session:", expect.any(Error));
    spy.mockRestore();
  });
});

describe("sessionsStore - searchSessions", () => {
  it("calls loadSessions when query is empty", async () => {
    setMockInvokeResult("list_sessions", []);
    await sessionsStore.searchSessions("");
    expect(get(sessionsStore.sessions)).toEqual([]);
  });

  it("calls loadSessions when query is whitespace-only", async () => {
    setMockInvokeResult("list_sessions", []);
    await sessionsStore.searchSessions("   ");
    expect(get(sessionsStore.sessions)).toEqual([]);
  });

  it("searches with the given query", async () => {
    const results = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }];
    setMockInvokeResult("search_sessions", results);
    await sessionsStore.searchSessions("test");
    expect(get(sessionsStore.sessions)).toEqual(results);
  });

  it("updates searchQuery store", async () => {
    setMockInvokeResult("search_sessions", []);
    await sessionsStore.searchSessions("hello");
    expect(get(sessionsStore.searchQuery)).toBe("hello");
    await sessionsStore.searchSessions("");
  });

  it("handles search errors gracefully", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("search_sessions", new Error("Search failed"));
    await sessionsStore.searchSessions("error-query");
    expect(spy).toHaveBeenCalledWith("Failed to search sessions:", expect.any(Error));
    spy.mockRestore();
  });
});

describe("sessionsStore - clearAllSessions", () => {
  it("clears all sessions", async () => {
    setMockInvokeResult("clear_all_sessions", undefined);
    await sessionsStore.clearAllSessions();
    expect(get(sessionsStore.sessions)).toEqual([]);
  });

  it("handles errors gracefully", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("clear_all_sessions", new Error("Failed"));
    await sessionsStore.clearAllSessions();
    expect(spy).toHaveBeenCalledWith("Failed to clear sessions:", expect.any(Error));
    spy.mockRestore();
  });
});

describe("sessionsStore - saveConversation", () => {
  it("saves conversation to backend", async () => {
    setMockInvokeResult("save_session", undefined);
    setMockInvokeResult("list_sessions", []);
    await sessionsStore.saveConversation(makeConversation() as never);
  });

  it("handles save errors gracefully", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("save_session", new Error("Save failed"));
    await sessionsStore.saveConversation(makeConversation() as never);
    expect(spy).toHaveBeenCalledWith("Failed to save session:", expect.any(Error));
    spy.mockRestore();
  });

  it("handles empty conversation", async () => {
    setMockInvokeResult("save_session", undefined);
    setMockInvokeResult("list_sessions", []);
    const conv = { ...makeConversation(), terminalLines: [] };
    await sessionsStore.saveConversation(conv as never);
  });
});

describe("sessionsStore - scheduleAutoSave and cancelAutoSave", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("schedules an auto-save after the debounce delay", async () => {
    setMockInvokeResult("save_session", undefined);
    setMockInvokeResult("list_sessions", []);
    sessionsStore.scheduleAutoSave(makeConversation() as never);
    await vi.advanceTimersByTimeAsync(2001);
  });

  it("does not auto-save empty conversations", async () => {
    const conv = { ...makeConversation(), terminalLines: [] };
    sessionsStore.scheduleAutoSave(conv as never);
    await vi.advanceTimersByTimeAsync(3000);
  });

  it("cancels a pending auto-save", () => {
    const conv = makeConversation();
    sessionsStore.scheduleAutoSave(conv as never);
    sessionsStore.cancelAutoSave(conv.id);
  });

  it("handles cancel when no auto-save is pending", () => {
    sessionsStore.cancelAutoSave("non-existent-id");
  });

  it("clears an existing pending auto-save when rescheduled for the same conversation", async () => {
    setMockInvokeResult("save_session", undefined);
    setMockInvokeResult("list_sessions", []);
    const conv = makeConversation();
    sessionsStore.scheduleAutoSave(conv as never);
    // Schedule again before timer fires — hits clearTimeout branch (line 487)
    sessionsStore.scheduleAutoSave(conv as never);
    await vi.advanceTimersByTimeAsync(2001);
  });
});

describe("sessionsStore - exportSessionAsJson", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns true on successful export", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.json");
    expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(true);
  });

  it("returns false when session not found", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", null);
    expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when user cancels save dialog", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue(null);
    expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
  });

  it("returns false on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", new Error("Load failed"));
    expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when writeTextFile throws", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.json");
    vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
    expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
    spy.mockRestore();
  });
});

describe("sessionsStore - exportSessionAsMarkdown", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("generates markdown with all message types", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    const session = makeSavedSession({
      messages: [
        { id: "1", type: "user", content: "User message", timestamp: "2026-03-03T10:00:00Z" },
        {
          id: "2",
          type: "assistant",
          content: "Assistant reply",
          timestamp: "2026-03-03T10:01:00Z",
        },
        {
          id: "3",
          type: "tool_use",
          content: "Tool input",
          timestamp: "2026-03-03T10:02:00Z",
          tool_name: "bash",
        },
        { id: "4", type: "tool_result", content: "Tool output", timestamp: "2026-03-03T10:03:00Z" },
        { id: "5", type: "system", content: "System event", timestamp: "2026-03-03T10:04:00Z" },
        { id: "6", type: "error", content: "Error message", timestamp: "2026-03-03T10:05:00Z" },
      ],
    });
    setMockInvokeResult("load_session", session);
    vi.mocked(save).mockResolvedValue("/output/session.md");
    expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(true);
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("User message");
    expect(content).toContain("Assistant reply");
    expect(content).toContain("Tool: bash");
    expect(content).toContain("Tool input");
    expect(content).toContain("Tool output");
    expect(content).toContain("System event");
    expect(content).toContain("Error message");
  });

  it("returns false when session not found", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", null);
    expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when user cancels dialog", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue(null);
    expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
  });

  it("returns false on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", new Error("Load failed"));
    expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when writeTextFile throws", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.md");
    vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
    expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
    spy.mockRestore();
  });
});

describe("sessionsStore - exportSessionAsHtml (tests escapeHtml + generateHtmlExport)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("generates HTML and returns true on success", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.html");
    expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(true);
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("<!DOCTYPE html>");
    expect(content).toContain("Test Session");
  });

  it("escapes HTML characters in session name", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult("load_session", makeSavedSession({ name: "<Test & 'Session'>" }));
    vi.mocked(save).mockResolvedValue("/output/session.html");
    await sessionsStore.exportSessionAsHtml("session-1");
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("&lt;Test &amp; &#039;Session&#039;&gt;");
    expect(content).not.toContain("<Test & 'Session'>");
  });

  it("escapes HTML characters in message content", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult(
      "load_session",
      makeSavedSession({
        messages: [
          {
            id: "1",
            type: "user",
            content: '<script>alert("xss")</script>',
            timestamp: "2026-03-03T10:00:00Z",
          },
        ],
      })
    );
    vi.mocked(save).mockResolvedValue("/output/session.html");
    await sessionsStore.exportSessionAsHtml("session-1");
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;");
    expect(content).not.toContain("<script>");
  });

  it("escapes HTML in tool names", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult(
      "load_session",
      makeSavedSession({
        messages: [
          {
            id: "1",
            type: "tool_use",
            content: "input",
            timestamp: "2026-03-03T10:00:00Z",
            tool_name: "<dangerous>",
          },
        ],
      })
    );
    vi.mocked(save).mockResolvedValue("/output/session.html");
    await sessionsStore.exportSessionAsHtml("session-1");
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("&lt;dangerous&gt;");
    expect(content).not.toContain("<dangerous>");
  });

  it("excludes metadata when includeMetadata is false", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.html");
    await sessionsStore.exportSessionAsHtml("session-1", false);
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).not.toContain('class="metadata"');
  });

  it("returns false when session not found", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", null);
    expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when user cancels dialog", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue(null);
    expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
  });

  it("returns false on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", new Error("Load failed"));
    expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when writeTextFile throws", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session.html");
    vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
    expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
    spy.mockRestore();
  });
});

describe("sessionsStore - exportSessionAsPdf (tests generatePrintableHtml)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("generates printable HTML and opens it in the browser", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { openPath } = await import("@tauri-apps/plugin-opener");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session-print.html");
    expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(true);
    expect(openPath).toHaveBeenCalledWith("/output/session-print.html");
    const content = vi.mocked(writeTextFile).mock.calls[0][1] as string;
    expect(content).toContain("<!DOCTYPE html>");
  });

  it("excludes metadata when includeMetadata is false", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session-print.html");
    await sessionsStore.exportSessionAsPdf("session-1", false);
    expect(vi.mocked(writeTextFile).mock.calls[0][1]).toBeDefined();
  });

  it("returns false when session not found", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", null);
    expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when user cancels dialog", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue(null);
    expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
  });

  it("returns false on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", new Error("Load failed"));
    expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
    spy.mockRestore();
  });

  it("returns false when writeTextFile throws", async () => {
    const { save } = await import("@tauri-apps/plugin-dialog");
    const { writeTextFile } = await import("@tauri-apps/plugin-fs");
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("load_session", makeSavedSession());
    vi.mocked(save).mockResolvedValue("/output/session-print.html");
    vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
    expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
    spy.mockRestore();
  });
});

describe("sessionsStore - importSession", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("imports session from JSON file and returns true", async () => {
    const { open } = await import("@tauri-apps/plugin-dialog");
    const { readTextFile } = await import("@tauri-apps/plugin-fs");
    vi.mocked(open).mockResolvedValue("/input/session.json");
    vi.mocked(readTextFile).mockResolvedValue(JSON.stringify({ session: makeSavedSession() }));
    setMockInvokeResult("save_session", undefined);
    setMockInvokeResult("list_sessions", []);
    expect(await sessionsStore.importSession()).toBe(true);
  });

  it("returns false when user cancels file dialog", async () => {
    const { open } = await import("@tauri-apps/plugin-dialog");
    vi.mocked(open).mockResolvedValue(null);
    expect(await sessionsStore.importSession()).toBe(false);
  });

  it("returns false when file has invalid format", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    const { open } = await import("@tauri-apps/plugin-dialog");
    const { readTextFile } = await import("@tauri-apps/plugin-fs");
    vi.mocked(open).mockResolvedValue("/input/bad.json");
    vi.mocked(readTextFile).mockResolvedValue(JSON.stringify({ not_a_session: true }));
    expect(await sessionsStore.importSession()).toBe(false);
    spy.mockRestore();
  });

  it("returns false on error", async () => {
    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    const { open } = await import("@tauri-apps/plugin-dialog");
    vi.mocked(open).mockRejectedValue(new Error("Dialog failed"));
    expect(await sessionsStore.importSession()).toBe(false);
    spy.mockRestore();
  });
});
