import { describe, expect, it } from "vitest";
import type { Tab, TabsState } from "./tabs";
import {
	activeTab,
	closeTab,
	createTab,
	deriveTitle,
	initialState,
	isStuckToEnd,
	MIN_HANDLE_LENGTH,
	newDraft,
	selectTab,
	setModel,
	setTitle,
	shortHandle,
} from "./tabs";

const tab = (conversationId: string, model = "default", title = "Chat"): Tab => ({
	conversationId,
	model,
	title,
});

describe("initialState", () => {
	it("returns empty draft state when no persisted state", () => {
		const state = initialState();
		expect(state.tabs).toEqual([]);
		expect(state.activeConversationId).toBeNull();
	});

	it("returns persisted state when provided", () => {
		const persisted: TabsState = {
			tabs: [tab("c1")],
			activeConversationId: "c1",
		};
		const state = initialState(persisted);
		expect(state.tabs).toHaveLength(1);
		expect(state.activeConversationId).toBe("c1");
	});
});

describe("newDraft", () => {
	it("sets activeConversationId to null", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" };
		const next = newDraft(state);
		expect(next.activeConversationId).toBeNull();
	});

	it("keeps existing tabs", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" };
		const next = newDraft(state);
		expect(next.tabs).toHaveLength(2);
	});
});

describe("createTab", () => {
	it("appends and activates", () => {
		const state = initialState();
		const next = createTab(state, tab("c1"));
		expect(next.tabs).toHaveLength(1);
		expect(next.tabs[0]?.conversationId).toBe("c1");
		expect(next.activeConversationId).toBe("c1");
	});

	it("does not duplicate an existing conversationId", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" };
		const next = createTab(state, tab("c1"));
		expect(next.tabs).toHaveLength(1);
	});

	it("activates an already-existing tab when createTab is called again", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" };
		const next = createTab(state, tab("c1"));
		expect(next.activeConversationId).toBe("c1");
	});
});

describe("selectTab", () => {
	it("changes active", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" };
		const next = selectTab(state, "c2");
		expect(next.activeConversationId).toBe("c2");
	});
});

describe("closeTab", () => {
	it("removes the tab", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" };
		const next = closeTab(state, "c2");
		expect(next.tabs).toHaveLength(1);
		expect(next.tabs[0]?.conversationId).toBe("c1");
	});

	it("closing the active tab activates a neighbour (previous preferred)", () => {
		const state: TabsState = {
			tabs: [tab("c1"), tab("c2"), tab("c3")],
			activeConversationId: "c2",
		};
		const next = closeTab(state, "c2");
		expect(next.activeConversationId).toBe("c1");
	});

	it("closing the first active tab activates the next", () => {
		const state: TabsState = {
			tabs: [tab("c1"), tab("c2"), tab("c3")],
			activeConversationId: "c1",
		};
		const next = closeTab(state, "c1");
		expect(next.activeConversationId).toBe("c2");
	});

	it("closing the last tab returns to draft (null active)", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" };
		const next = closeTab(state, "c1");
		expect(next.tabs).toHaveLength(0);
		expect(next.activeConversationId).toBeNull();
	});

	it("closing a non-active tab does not change active", () => {
		const state: TabsState = {
			tabs: [tab("c1"), tab("c2"), tab("c3")],
			activeConversationId: "c3",
		};
		const next = closeTab(state, "c1");
		expect(next.activeConversationId).toBe("c3");
	});

	it("closing a non-existent tab is a no-op", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" };
		const next = closeTab(state, "missing");
		expect(next).toEqual(state);
	});
});

describe("setModel", () => {
	it("updates the right tab", () => {
		const state: TabsState = { tabs: [tab("c1", "old"), tab("c2")], activeConversationId: "c1" };
		const next = setModel(state, "c1", "new-model");
		expect(next.tabs[0]?.model).toBe("new-model");
		expect(next.tabs[1]?.model).toBe("default");
	});
});

describe("setTitle", () => {
	it("updates the right tab", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" };
		const next = setTitle(state, "c1", "Updated title");
		expect(next.tabs[0]?.title).toBe("Updated title");
		expect(next.tabs[1]?.title).toBe("Chat");
	});
});

describe("activeTab", () => {
	it("returns the active tab", () => {
		const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" };
		expect(activeTab(state)?.conversationId).toBe("c2");
	});

	it("returns null when activeConversationId is null", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: null };
		expect(activeTab(state)).toBeNull();
	});

	it("returns null when active tab is not found in tabs", () => {
		const state: TabsState = { tabs: [tab("c1")], activeConversationId: "missing" };
		expect(activeTab(state)).toBeNull();
	});
});

describe("deriveTitle", () => {
	it("truncates long messages with ellipsis", () => {
		const msg = "This is a very long message that should be truncated at some point";
		expect(deriveTitle(msg, 20)).toBe("This is a very long \u2026");
	});

	it("returns full message when under max", () => {
		expect(deriveTitle("Short", 40)).toBe("Short");
	});

	it("collapses whitespace", () => {
		expect(deriveTitle("  hello   world  ")).toBe("hello world");
	});

	it("falls back to 'New chat' for empty input", () => {
		expect(deriveTitle("")).toBe("New chat");
		expect(deriveTitle("   ")).toBe("New chat");
	});

	it("uses default max of ~40 chars", () => {
		const msg = "a".repeat(50);
		const result = deriveTitle(msg);
		expect(result).toBe(`${"a".repeat(40)}\u2026`);
	});
});

describe("isStuckToEnd", () => {
	it("is false when the strip does not overflow", () => {
		expect(isStuckToEnd({ scrollLeft: 0, clientWidth: 500, scrollWidth: 500 })).toBe(false);
		expect(isStuckToEnd({ scrollLeft: 0, clientWidth: 500, scrollWidth: 400 })).toBe(false);
	});

	it("is true when overflowing and scrolled to the left", () => {
		expect(isStuckToEnd({ scrollLeft: 0, clientWidth: 500, scrollWidth: 1000 })).toBe(true);
	});

	it("is true when overflowing and scrolled to the middle", () => {
		expect(isStuckToEnd({ scrollLeft: 250, clientWidth: 500, scrollWidth: 1000 })).toBe(true);
	});

	it("is false when overflowing but scrolled fully to the right", () => {
		expect(isStuckToEnd({ scrollLeft: 500, clientWidth: 500, scrollWidth: 1000 })).toBe(false);
	});

	it("treats a 1px subpixel gap at the end as at-rest (epsilon)", () => {
		expect(isStuckToEnd({ scrollLeft: 499, clientWidth: 500, scrollWidth: 1000 })).toBe(false);
	});
});

describe("shortHandle", () => {
	it("uses the minimum length when the id is unique", () => {
		const h = shortHandle("3f9a1b2c-aaaa", ["3f9a1b2c-aaaa", "7c2d-bbbb"]);
		expect(h).toBe("3f9a");
		expect(h.length).toBe(MIN_HANDLE_LENGTH);
	});

	it("grows the prefix until unique among open tabs", () => {
		// two ids share the first 5 chars → handle grows to 6 to disambiguate
		expect(shortHandle("abcde1-xxxx", ["abcde1-xxxx", "abcde2-yyyy"])).toBe("abcde1");
		expect(shortHandle("abcde2-yyyy", ["abcde1-xxxx", "abcde2-yyyy"])).toBe("abcde2");
	});

	it("shrinks back to the minimum when the colliding sibling is gone", () => {
		expect(shortHandle("abcde1-xxxx", ["abcde1-xxxx"])).toBe("abcd");
	});

	it("ignores the id itself when present in the list", () => {
		expect(shortHandle("deadbeef", ["deadbeef"])).toBe("dead");
	});

	it("returns the whole id when shorter than the minimum length", () => {
		expect(shortHandle("ab", ["ab", "cd"])).toBe("ab");
	});

	it("falls back to the full id when one id is a prefix of another", () => {
		// "abcd" is a prefix of "abcd1234" → no unique shorter prefix exists for it
		expect(shortHandle("abcd", ["abcd", "abcd1234"])).toBe("abcd");
	});
});
