import { fireEvent, render } from "@solidjs/testing-library";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "../mocks/tauri";

const {
	mockToggleExpanded,
	mockToggleCollapsed,
	mockGetActive,
	mockGetOrderedRepos,
	mockReorderRepo,
	mockTerminalsGet,
	mockGetCheckSummary,
	mockGetPrStatus,
	mockGetGroupedLayout,
	mockGetGroupForRepo,
	mockToggleGroupCollapsed,
	mockDeleteGroup,
	mockAddRepoToGroup,
	mockRemoveRepoFromGroup,
	mockCreateGroup,
	mockReorderRepoInGroup,
	mockMoveRepoBetweenGroups,
	mockReorderGroups,
} = vi.hoisted(() => ({
	mockToggleExpanded: vi.fn(),
	mockToggleCollapsed: vi.fn(),
	mockGetActive: vi.fn<() => unknown>(() => null),
	mockGetOrderedRepos: vi.fn<() => unknown[]>(() => []),
	mockReorderRepo: vi.fn(),
	mockTerminalsGet: vi.fn<() => unknown>(() => null),
	mockGetCheckSummary: vi.fn<() => unknown>(() => null),
	mockGetPrStatus: vi.fn<(...args: unknown[]) => unknown>(() => null),
	mockGetGroupedLayout: vi.fn<() => unknown>(() => ({ groups: [], ungrouped: [] })),
	mockGetGroupForRepo: vi.fn<(path: string) => unknown>(() => undefined),
	mockToggleGroupCollapsed: vi.fn(),
	mockDeleteGroup: vi.fn(),
	mockAddRepoToGroup: vi.fn(),
	mockRemoveRepoFromGroup: vi.fn(),
	mockCreateGroup: vi.fn(() => "new-group-id"),
	mockReorderRepoInGroup: vi.fn(),
	mockMoveRepoBetweenGroups: vi.fn(),
	mockReorderGroups: vi.fn(),
}));

// Mock stores before importing the component
vi.mock("../../stores/repositories", () => ({
	repositoriesStore: {
		state: {
			repositories: {} as Record<string, unknown>,
			repoOrder: [] as string[],
			activeRepoPath: null as string | null,
			groups: {} as Record<string, unknown>,
			groupOrder: [] as string[],
		},
		getActive: mockGetActive,
		getOrderedRepos: mockGetOrderedRepos,
		reorderRepo: mockReorderRepo,
		toggleExpanded: mockToggleExpanded,
		toggleCollapsed: mockToggleCollapsed,
		getGroupedLayout: mockGetGroupedLayout,
		getGroupForRepo: mockGetGroupForRepo,
		toggleGroupCollapsed: mockToggleGroupCollapsed,
		deleteGroup: mockDeleteGroup,
		addRepoToGroup: mockAddRepoToGroup,
		removeRepoFromGroup: mockRemoveRepoFromGroup,
		createGroup: mockCreateGroup,
		renameGroup: vi.fn(() => true),
		setGroupColor: vi.fn(),
		reorderRepoInGroup: mockReorderRepoInGroup,
		moveRepoBetweenGroups: mockMoveRepoBetweenGroups,
		reorderGroups: mockReorderGroups,
		getParkedRepos: vi.fn(() => []),
		setPark: vi.fn(),
		setParkGroup: vi.fn(),
		isGroupFullyParked: vi.fn(() => false),
		get: vi.fn(() => undefined),
		setActive: vi.fn(),
	},
}));

vi.mock("../../stores/repoSettings", () => ({
	repoSettingsStore: {
		get: vi.fn(() => undefined),
		getEffective: vi.fn(() => undefined),
		setLabel: vi.fn(),
	},
}));

vi.mock("../../stores/terminals", () => ({
	terminalsStore: {
		get: mockTerminalsGet,
		isBusy: vi.fn(() => false),
		onRemove: vi.fn(() => () => {}),
	},
}));

vi.mock("../../stores/github", () => ({
	githubStore: {
		getCheckSummary: mockGetCheckSummary,
		getPrStatus: mockGetPrStatus,
		getRemoteOnlyPrs: vi.fn(() => []),
		getRepoIssues: vi.fn(() => []),
		getAllOpenPrs: vi.fn(() => []),
		state: { viewerLogin: null, issuesLoading: false, circuitBreakerOpen: false },
	},
}));

const { mockLastActivityAt } = vi.hoisted(() => ({
	mockLastActivityAt: vi.fn<() => number>(() => 0),
}));

vi.mock("../../stores/userActivity", () => ({
	userActivityStore: {
		lastActivityAt: mockLastActivityAt,
	},
}));

// Mock PrDetailPopover to avoid cascading store dependencies
vi.mock("../../components/PrDetailPopover/PrDetailPopover", () => ({
	PrDetailPopover: (props: { repoPath: string; branch: string; onClose: () => void }) => (
		<div data-testid="pr-detail-popover" data-branch={props.branch} data-repo={props.repoPath} />
	),
}));

import { _resetMergedActivityAccum } from "../../components/Sidebar/RepoSection";
import { Sidebar } from "../../components/Sidebar/Sidebar";
import { repositoriesStore } from "../../stores/repositories";
import { uiStore } from "../../stores/ui";

/** Helper to create default no-op props for Sidebar */
function defaultProps(overrides: Partial<Parameters<typeof Sidebar>[0]> = {}) {
	return {
		onBranchSelect: vi.fn(),
		onAddTerminal: vi.fn(),
		onRemoveBranch: vi.fn(),
		onRenameBranch: vi.fn(),
		onAddWorktree: vi.fn(),
		onAddRepo: vi.fn(),
		onRepoSettings: vi.fn(),
		onRemoveRepo: vi.fn(),
		onOpenSettings: vi.fn(),
		onOpenHelp: vi.fn(),
		onBackgroundGit: vi.fn(),
		...overrides,
	};
}

/** Helper to create a repository with branches */
function makeRepo(overrides: Record<string, unknown> = {}) {
	return {
		path: "/repo1",
		displayName: "Repo One",
		initials: "RO",
		expanded: true,
		collapsed: false,
		activeBranch: "main",
		branches: {
			main: {
				name: "main",
				isMain: true,
				worktreePath: null,
				terminals: [],
				additions: 0,
				deletions: 0,
			},
		},
		order: 0,
		...overrides,
	};
}

/** Helper to set repo store state */
function setRepos(repos: Record<string, unknown>, activeRepoPath?: string) {
	(repositoriesStore.state as { repositories: Record<string, unknown> }).repositories = repos;
	(repositoriesStore.state as { activeRepoPath: string | null }).activeRepoPath =
		activeRepoPath ?? Object.keys(repos)[0] ?? null;
	const repoValues = Object.values(repos);
	mockGetOrderedRepos.mockReturnValue(repoValues);
	// Default grouped layout: all repos ungrouped (backward compatible)
	mockGetGroupedLayout.mockReturnValue({ groups: [], ungrouped: repoValues });
}

describe("Sidebar", () => {
	beforeEach(() => {
		vi.useFakeTimers();
		vi.clearAllMocks();
		setRepos({});
		mockGetActive.mockReturnValue(null);
		mockTerminalsGet.mockReturnValue(null);
		mockLastActivityAt.mockReturnValue(0);
		_resetMergedActivityAccum();
	});

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

	describe("empty state", () => {
		it("renders 'No repositories' and 'Add Repository' button when no repos exist", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const emptyDiv = container.querySelector(".empty");
			expect(emptyDiv).not.toBeNull();
			expect(emptyDiv!.textContent).toContain("No repositories");

			const addButton = emptyDiv!.querySelector("button");
			expect(addButton).not.toBeNull();
			expect(addButton!.textContent).toBe("Add Repository");
		});

		it("calls onAddRepo when empty-state Add Repository button is clicked", () => {
			const onAddRepo = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddRepo })} />);

			const addButton = container.querySelector(".empty button");
			expect(addButton).not.toBeNull();
			fireEvent.click(addButton!);
			expect(onAddRepo).toHaveBeenCalledOnce();
		});
	});

	describe("footer buttons", () => {
		it("calls onAddRepo when footer Add Repository button is clicked", () => {
			const onAddRepo = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddRepo })} />);

			const footerAddBtn = container.querySelector(".addRepo");
			expect(footerAddBtn).not.toBeNull();
			fireEvent.click(footerAddBtn!);
			expect(onAddRepo).toHaveBeenCalled();
		});

		it("calls onOpenSettings when Settings footer button is clicked", () => {
			const onOpenSettings = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onOpenSettings })} />);

			const settingsBtn = container.querySelector('.footerAction[title="Settings"]');
			expect(settingsBtn).not.toBeNull();
			fireEvent.click(settingsBtn!);
			expect(onOpenSettings).toHaveBeenCalledOnce();
		});

		it("calls onOpenHelp when Help footer button is clicked", () => {
			const onOpenHelp = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onOpenHelp })} />);

			const helpBtn = container.querySelector('.footerAction[title="Help"]');
			expect(helpBtn).not.toBeNull();
			fireEvent.click(helpBtn!);
			expect(onOpenHelp).toHaveBeenCalledOnce();
		});

		it("does not render unimplemented Notifications and Tasks buttons", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			expect(container.querySelector('.footerAction[title="Notifications"]')).toBeNull();
			expect(container.querySelector('.footerAction[title="Tasks"]')).toBeNull();
		});
	});

	describe("with repositories", () => {
		beforeEach(() => {
			setRepos({ "/repo1": makeRepo() });
		});

		it("renders repo sections when repos exist", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const repoSections = container.querySelectorAll(".repoSection");
			expect(repoSections.length).toBe(1);

			// Should NOT show empty state
			const emptyDiv = container.querySelector(".empty");
			expect(emptyDiv).toBeNull();
		});

		it("shows repo display name", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const repoName = container.querySelector(".repoName");
			expect(repoName).not.toBeNull();
			expect(repoName!.textContent).toBe("Repo One");
		});

		it("renders branch items for expanded repo", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const branchItems = container.querySelectorAll(".branchItem");
			expect(branchItems.length).toBe(1);

			const branchName = container.querySelector(".branchName");
			expect(branchName).not.toBeNull();
			expect(branchName!.textContent).toBe("main");
		});

		it("shows SVG icons for main and feature branches", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: ["t1"], additions: 0, deletions: 0 },
						"feature/x": {
							name: "feature/x",
							isMain: false,
							worktreePath: "/wt/x",
							terminals: ["t2"],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const icons = container.querySelectorAll(".branchIcon");
			expect(icons.length).toBe(2);
			// Main branch first (sorted), then feature
			const mainIcon = Array.from(icons).find((i) => i.classList.contains("branchIconMain"));
			const featureIcon = Array.from(icons).find((i) => i.classList.contains("branchIconWorktree"));
			expect(mainIcon).toBeDefined();
			expect(mainIcon!.querySelector("svg")).not.toBeNull();
			expect(featureIcon).toBeDefined();
			expect(featureIcon!.querySelector("svg")).not.toBeNull();
		});

		it("sorts branches with main first", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						"feature/z": {
							name: "feature/z",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/a": {
							name: "feature/a",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const names = container.querySelectorAll(".branchName");
			expect(names.length).toBe(3);
			expect(names[0].textContent).toBe("main");
			expect(names[1].textContent).toBe("feature/a");
			expect(names[2].textContent).toBe("feature/z");
		});

		it("sorts merged PR branches to bottom", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/active": {
							name: "feature/active",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
						"feature/merged": {
							name: "feature/merged",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
						"feature/closed": {
							name: "feature/closed",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			// Mock merged and closed PR states
			mockGetPrStatus.mockImplementation((_repoPath: unknown, branch: unknown) => {
				if (branch === "feature/merged") return { state: "MERGED", number: 1, title: "", url: "" };
				if (branch === "feature/closed") return { state: "CLOSED", number: 2, title: "", url: "" };
				if (branch === "feature/active") return { state: "OPEN", number: 3, title: "", url: "" };
				return null;
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const names = container.querySelectorAll(".branchName");
			expect(names.length).toBe(4);
			expect(names[0].textContent).toBe("main");
			expect(names[1].textContent).toBe("feature/active");
			// Merged/closed at bottom, alphabetically
			expect(names[2].textContent).toBe("feature/closed");
			expect(names[3].textContent).toBe("feature/merged");
		});

		it("marks active branch with active class", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItem = container.querySelector(".branchItem");
			expect(branchItem).not.toBeNull();
			expect(branchItem!.classList.contains("active")).toBe(true);
		});

		it("branch item click calls onBranchSelect", () => {
			const onBranchSelect = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onBranchSelect })} />);
			const branchItem = container.querySelector(".branchItem")!;
			fireEvent.click(branchItem);
			expect(onBranchSelect).toHaveBeenCalledWith("/repo1", "main");
		});

		it("add terminal button click calls onAddTerminal", () => {
			const onAddTerminal = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddTerminal })} />);
			const addBtn = container.querySelector(".branchAddBtn")!;
			fireEvent.click(addBtn);
			expect(onAddTerminal).toHaveBeenCalledWith("/repo1", "main");
		});

		it("main branch has no remove button", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const removeBtn = container.querySelector(".branchRemoveBtn");
			expect(removeBtn).toBeNull();
		});

		it("feature branch has remove button that calls onRemoveBranch", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/x": {
							name: "feature/x",
							isMain: false,
							worktreePath: "/wt/x",
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const onRemoveBranch = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onRemoveBranch })} />);
			const removeBtns = container.querySelectorAll(".branchRemoveBtn");
			expect(removeBtns.length).toBe(1);
			fireEvent.click(removeBtns[0]);
			expect(onRemoveBranch).toHaveBeenCalledWith("/repo1", "feature/x");
		});

		it("non-main branch without worktreePath has no remove button", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/y": {
							name: "feature/y",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const removeBtns = container.querySelectorAll(".branchRemoveBtn");
			expect(removeBtns.length).toBe(0);
		});

		it("double-click main branch name calls onAddTerminal instead of rename", () => {
			const onAddTerminal = vi.fn();
			const onRenameBranch = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddTerminal, onRenameBranch })} />);
			const branchName = container.querySelector(".branchName")!;
			fireEvent.dblClick(branchName);
			expect(onRenameBranch).not.toHaveBeenCalled();
			expect(onAddTerminal).toHaveBeenCalledWith("/repo1", "main");
		});

		it("double-click feature branch name calls onRenameBranch", () => {
			const onRenameBranch = vi.fn();
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/x": {
							name: "feature/x",
							isMain: false,
							worktreePath: "/wt/x",
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps({ onRenameBranch })} />);
			const branchNames = container.querySelectorAll(".branchName");
			// feature/x is the second branch
			const featureBranch = branchNames[1]!;
			fireEvent.dblClick(featureBranch);
			expect(onRenameBranch).toHaveBeenCalledWith("/repo1", "feature/x");
		});

		it("add worktree button click calls onAddWorktree", () => {
			const onAddWorktree = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddWorktree })} />);
			const addBtn = container.querySelector(".addBtn")!;
			fireEvent.click(addBtn);
			expect(onAddWorktree).toHaveBeenCalledWith("/repo1");
		});

		it("repo menu opens on click and shows Settings and Remove options", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			// Click the ⋯ button
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);

			const menu = container.querySelector(".menu");
			expect(menu).not.toBeNull();

			const menuItems = menu!.querySelectorAll(".item");
			expect(menuItems.length).toBe(5);
			expect(menuItems[0].textContent).toContain("Repo Settings");
			expect(menuItems[1].textContent).toContain("Create Worktree");
			expect(menuItems[2].textContent).toContain("Move to Group");
			expect(menuItems[3].textContent).toContain("Park Repository");
			expect(menuItems[4].textContent).toContain("Remove Repository");
		});

		it("repo menu Settings click calls onRepoSettings", () => {
			const onRepoSettings = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onRepoSettings })} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);

			// Click settings
			const menuItems = container.querySelectorAll(".item");
			fireEvent.click(menuItems[0]);
			expect(onRepoSettings).toHaveBeenCalledWith("/repo1");
		});

		it("repo menu Remove click calls onRemoveRepo", () => {
			const onRemoveRepo = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onRemoveRepo })} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);

			// Click remove — index 4 (after "Repo Settings", "Create Worktree", "Move to Group", and "Park Repository")
			const menuItems = container.querySelectorAll(".item");
			fireEvent.click(menuItems[4]);
			expect(onRemoveRepo).toHaveBeenCalledWith("/repo1");
		});

		it("repo menu Create Worktree click calls onAddWorktree", () => {
			const onAddWorktree = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onAddWorktree })} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);

			// Click Create Worktree — index 1
			const menuItems = container.querySelectorAll(".item");
			fireEvent.click(menuItems[1]);
			expect(onAddWorktree).toHaveBeenCalledWith("/repo1");
		});

		it("repo menu does not show Create Worktree for non-git repos", () => {
			setRepos({ "/repo1": makeRepo({ isGitRepo: false }) });
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);

			const menu = container.querySelector(".menu");
			expect(menu).not.toBeNull();

			const menuItems = menu!.querySelectorAll(".item");
			expect(menuItems.length).toBe(4);
			const labels = Array.from(menuItems).map((el) => el.textContent);
			expect(labels.some((l) => l?.includes("Create Worktree"))).toBe(false);
		});

		it("repo menu closes after action", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);
			expect(container.querySelector(".menu")).not.toBeNull();

			// Click settings to close menu
			const menuItems = container.querySelectorAll(".item");
			fireEvent.click(menuItems[0]);

			// Menu should be closed
			expect(container.querySelector(".menu")).toBeNull();
		});

		it("repo menu closes on Escape", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);
			expect(container.querySelector(".menu")).not.toBeNull();

			// Press Escape
			fireEvent.keyDown(document, { key: "Escape" });
			expect(container.querySelector(".menu")).toBeNull();
		});

		it("repo menu toggles on repeated clicks", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const menuBtn = container.querySelector(".repoActionBtn")!;

			// Open
			fireEvent.click(menuBtn);
			expect(container.querySelector(".menu")).not.toBeNull();

			// Close
			fireEvent.click(menuBtn);
			expect(container.querySelector(".menu")).toBeNull();
		});

		it("repo header right-click opens context menu with Settings and Remove options", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".repoHeader")!;
			fireEvent.contextMenu(header, { clientX: 100, clientY: 200 });

			const menu = container.querySelector(".menu");
			expect(menu).not.toBeNull();

			const menuItems = menu!.querySelectorAll(".item");
			expect(menuItems.length).toBe(5);
			expect(menuItems[0].textContent).toContain("Repo Settings");
			expect(menuItems[1].textContent).toContain("Create Worktree");
			expect(menuItems[2].textContent).toContain("Move to Group");
			expect(menuItems[3].textContent).toContain("Park Repository");
			expect(menuItems[4].textContent).toContain("Remove Repository");
		});

		it("branch Copy Path item exposes the full path as a native tooltip", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/x": {
							name: "feature/x",
							isMain: false,
							worktreePath: "/wt/feature-x",
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const branchRow = Array.from(container.querySelectorAll(".branchItem")).find((el) =>
				el.textContent?.includes("feature/x"),
			)!;
			fireEvent.contextMenu(branchRow, { clientX: 50, clientY: 50 });

			const copyPath = Array.from(container.querySelectorAll(".menu .item")).find((el) =>
				el.textContent?.includes("Copy Path"),
			)!;
			expect(copyPath).toBeTruthy();
			expect(copyPath.getAttribute("title")).toBe("/wt/feature-x");
		});

		it("repo header click calls toggleExpanded", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".repoHeader")!;
			fireEvent.click(header);
			expect(mockToggleExpanded).toHaveBeenCalledWith("/repo1");
		});

		it("shows 'No branches loaded' when repo has no branches", () => {
			setRepos({
				"/repo1": makeRepo({ branches: {} }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const empty = container.querySelector(".repoEmpty");
			expect(empty).not.toBeNull();
			expect(empty!.textContent).toBe("No branches loaded");
		});

		it("renders a chevron toggle in the repo header", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const chevron = container.querySelector(".repoChevron");
			expect(chevron).not.toBeNull();
		});

		it("chevron has expanded class when repo is expanded", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const chevron = container.querySelector(".repoChevron");
			expect(chevron!.classList.contains("expanded")).toBe(true);
		});

		it("chevron does not have expanded class when repo is not expanded", () => {
			setRepos({
				"/repo1": makeRepo({ expanded: false }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const chevron = container.querySelector(".repoChevron");
			expect(chevron).not.toBeNull();
			expect(chevron!.classList.contains("expanded")).toBe(false);
		});
	});

	describe("collapsed repo", () => {
		it("shows initials and not branches when collapsed", () => {
			setRepos({
				"/repo1": makeRepo({ collapsed: true }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const initials = container.querySelector(".repoInitials");
			expect(initials).not.toBeNull();
			expect(initials!.textContent).toBe("RO");

			// Should not show repo name or branches
			const repoName = container.querySelector(".repoName");
			expect(repoName).toBeNull();

			const branchItems = container.querySelectorAll(".branchItem");
			expect(branchItems.length).toBe(0);
		});

		it("shows collapsed class on repo section", () => {
			setRepos({
				"/repo1": makeRepo({ collapsed: true }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const section = container.querySelector(".repoSection");
			expect(section).not.toBeNull();
			expect(section!.classList.contains("collapsed")).toBe(true);
		});

		it("initials click calls toggleCollapsed", () => {
			setRepos({
				"/repo1": makeRepo({ collapsed: true }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const initials = container.querySelector(".repoInitials")!;
			fireEvent.click(initials);
			expect(mockToggleCollapsed).toHaveBeenCalledWith("/repo1");
		});
	});

	describe("unexpanded repo", () => {
		it("does not show branches when expanded is false and not collapsed", () => {
			setRepos({
				"/repo1": makeRepo({ expanded: false, collapsed: false }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItems = container.querySelectorAll(".branchItem");
			expect(branchItems.length).toBe(0);
			// But should still show repo name
			expect(container.querySelector(".repoName")!.textContent).toBe("Repo One");
		});
	});

	describe("Git Quick Actions", () => {
		it("shows git quick actions when getActive returns a repo", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			const quickActions = container.querySelector(".gitQuickActions");
			expect(quickActions).not.toBeNull();

			const buttons = container.querySelectorAll(".gitQuickBtn");
			expect(buttons.length).toBe(4);
		});

		it("does not show git quick actions when getActive returns null", () => {
			mockGetActive.mockReturnValue(null);
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const quickActions = container.querySelector(".gitQuickActions");
			expect(quickActions).toBeNull();
		});

		it("Pull button calls onBackgroundGit with pull args", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const onBackgroundGit = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onBackgroundGit })} />);

			const buttons = container.querySelectorAll(".gitQuickBtn");
			const pullBtn = Array.from(buttons).find((b) => b.textContent?.includes("Pull"))!;
			fireEvent.click(pullBtn);
			expect(onBackgroundGit).toHaveBeenCalledWith("/repo1", "pull", ["pull"]);
		});

		it("Push button calls onBackgroundGit with push args", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const onBackgroundGit = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onBackgroundGit })} />);

			const buttons = container.querySelectorAll(".gitQuickBtn");
			const pushBtn = Array.from(buttons).find((b) => b.textContent?.includes("Push"))!;
			fireEvent.click(pushBtn);
			expect(onBackgroundGit).toHaveBeenCalledWith("/repo1", "push", ["push"]);
		});

		it("Fetch button calls onBackgroundGit with fetch args", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const onBackgroundGit = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onBackgroundGit })} />);

			const buttons = container.querySelectorAll(".gitQuickBtn");
			const fetchBtn = Array.from(buttons).find((b) => b.textContent?.includes("Fetch"))!;
			fireEvent.click(fetchBtn);
			expect(onBackgroundGit).toHaveBeenCalledWith("/repo1", "fetch", ["fetch", "--all"]);
		});

		it("Stash button calls onBackgroundGit with stash args", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const onBackgroundGit = vi.fn();
			const { container } = render(() => <Sidebar {...defaultProps({ onBackgroundGit })} />);

			const buttons = container.querySelectorAll(".gitQuickBtn");
			const stashBtn = Array.from(buttons).find((b) => b.textContent?.includes("Stash"))!;
			fireEvent.click(stashBtn);
			expect(onBackgroundGit).toHaveBeenCalledWith("/repo1", "stash", ["stash"]);
		});

		it("disables button when operation is in runningGitOps", () => {
			mockGetActive.mockReturnValue({ path: "/repo1" });
			setRepos({ "/repo1": makeRepo() });
			const runningOps = new Set(["pull"]);
			const { container } = render(() => <Sidebar {...defaultProps({ runningGitOps: runningOps })} />);

			const buttons = container.querySelectorAll(".gitQuickBtn");
			const pullBtn = Array.from(buttons).find((b) => b.textContent?.includes("Pull"))!;
			expect(pullBtn.hasAttribute("disabled")).toBe(true);

			// Other buttons should not be disabled
			const pushBtn = Array.from(buttons).find((b) => b.textContent?.includes("Push"))!;
			expect(pushBtn.hasAttribute("disabled")).toBe(false);
		});
	});

	describe("quick switcher mode", () => {
		it("shows shortcut keys and hides action buttons in quick switcher mode", () => {
			setRepos({ "/repo1": makeRepo() });
			const { container } = render(() => <Sidebar {...defaultProps({ quickSwitcherActive: true })} />);

			const shortcut = container.querySelector(".branchShortcut") as HTMLElement;
			expect(shortcut).not.toBeNull();
			expect(shortcut.textContent).toContain("1");
			expect(shortcut.style.display).not.toBe("none");

			// Action buttons should be hidden (display:none), not removed
			const actions = container.querySelector(".branchActions") as HTMLElement;
			expect(actions).not.toBeNull();
			expect(actions.style.display).toBe("none");
		});

		it("keeps collapsed repos hidden in quick switcher mode", () => {
			setRepos({ "/repo1": makeRepo({ expanded: false }) });
			const { container } = render(() => <Sidebar {...defaultProps({ quickSwitcherActive: true })} />);

			const branchItems = container.querySelectorAll(".branchItem");
			expect(branchItems.length).toBe(0);
		});
	});

	describe("branch badges", () => {
		it("shows StatsBadge with additions and deletions", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 10, deletions: 5 },
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const stats = container.querySelector(".branchStats");
			expect(stats).not.toBeNull();
			const addStat = container.querySelector(".statAdd");
			const delStat = container.querySelector(".statDel");
			expect(addStat!.textContent).toBe("+10");
			expect(delStat!.textContent).toBe("-5");
		});

		it("does not show StatsBadge when both additions and deletions are 0", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const stats = container.querySelector(".branchStats");
			expect(stats).toBeNull();
		});

		it("shows StatsBadge when only additions > 0", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 5, deletions: 0 },
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const stats = container.querySelector(".branchStats");
			expect(stats).not.toBeNull();
		});

		it("shows StatsBadge when only deletions > 0", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 3 },
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const stats = container.querySelector(".branchStats");
			expect(stats).not.toBeNull();
		});

		it("shows PrStateBadge when GitHub store has PR data for branch", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			mockGetPrStatus.mockReturnValue({ state: "OPEN", number: 123, title: "Test PR", url: "https://example.com" });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).not.toBeNull();
			expect(prBadge!.getAttribute("title")).toBe("PR #123");
		});

		it("does not show PrStateBadge when branch has no PR data", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).toBeNull();
		});

		it("shows Merged label and class when PR state is MERGED", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			mockGetPrStatus.mockReturnValue({ state: "MERGED", number: 42, title: "Test", url: "https://example.com" });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).not.toBeNull();
			expect(prBadge!.classList.contains("prMerged")).toBe(true);
			expect(prBadge!.textContent).toBe("Merged");
		});

		it("hides PR badge immediately for CLOSED PR", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			mockGetPrStatus.mockReturnValue({ state: "CLOSED", number: 43, title: "Test", url: "https://example.com" });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).toBeNull();
		});

		it("shows Draft label and class when PR is a draft", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			mockGetPrStatus.mockReturnValue({
				state: "OPEN",
				number: 45,
				title: "Draft PR",
				url: "https://example.com",
				is_draft: true,
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).not.toBeNull();
			expect(prBadge!.classList.contains("prDraft")).toBe(true);
			expect(prBadge!.textContent).toBe("Draft");
		});

		it("shows open class with PR number when state is OPEN with no special conditions", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			mockGetPrStatus.mockReturnValue({ state: "OPEN", number: 44, title: "Test", url: "https://example.com" });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const prBadge = container.querySelector(".prBadge");
			expect(prBadge).not.toBeNull();
			expect(prBadge!.classList.contains("prOpen")).toBe(true);
			expect(prBadge!.textContent).toBe("#44");
		});
	});

	describe("branch activity indicator (via isBusy)", () => {
		it("does not add hasActivity class (activity flag no longer drives sidebar)", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: ["t1"], additions: 0, deletions: 0 },
					},
				}),
			});
			mockTerminalsGet.mockReturnValue({ activity: true });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItem = container.querySelector(".branchItem");
			expect(branchItem!.classList.contains("hasActivity")).toBe(false);
		});
	});

	describe("multiple repos", () => {
		it("renders multiple repo sections", () => {
			setRepos({
				"/repo1": makeRepo(),
				"/repo2": makeRepo({ path: "/repo2", displayName: "Repo Two", initials: "RT" }),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoSections = container.querySelectorAll(".repoSection");
			expect(repoSections.length).toBe(2);
		});
	});

	describe("click outside repo menu", () => {
		it("closes repo menu when clicking outside", () => {
			setRepos({ "/repo1": makeRepo() });
			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// Open menu
			const menuBtn = container.querySelector(".repoActionBtn")!;
			fireEvent.click(menuBtn);
			expect(container.querySelector(".menu")).not.toBeNull();

			// Click somewhere outside the menu (on the sidebar itself)
			fireEvent.mouseDown(container.querySelector("[data-testid='sidebar']")!);
			expect(container.querySelector(".menu")).toBeNull();
		});
	});

	describe("context menu", () => {
		it("opens context menu on right-click of branch item", () => {
			setRepos({ "/repo1": makeRepo() });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItem = container.querySelector(".branchItem")!;
			fireEvent.contextMenu(branchItem, { clientX: 100, clientY: 200 });

			const contextMenu = container.querySelector(".menu");
			expect(contextMenu).not.toBeNull();

			const items = contextMenu!.querySelectorAll(".item");
			// Copy Path, Add Terminal, Set Label…, Rename Branch (no Delete Worktree for main)
			expect(items.length).toBe(4);
		});

		it("context menu Copy Path action copies worktreePath to clipboard", async () => {
			const writeTextMock = vi.fn().mockResolvedValue(undefined);
			Object.defineProperty(navigator, "clipboard", {
				value: { writeText: writeTextMock },
				writable: true,
				configurable: true,
			});

			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: {
							name: "main",
							isMain: true,
							worktreePath: "/path/to/repo",
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItem = container.querySelector(".branchItem")!;
			fireEvent.contextMenu(branchItem, { clientX: 100, clientY: 200 });

			const contextMenu = container.querySelector(".menu");
			const items = contextMenu!.querySelectorAll(".item");
			// Find "Copy Path" item
			const copyPathItem = Array.from(items).find((i) => i.querySelector(".label")?.textContent === "Copy Path")!;
			fireEvent.click(copyPathItem);

			// The action is async (uses navigator.clipboard.writeText)
			await vi.waitFor(() => {
				expect(writeTextMock).toHaveBeenCalledWith("/path/to/repo");
			});
		});

		it("context menu Copy Path is disabled when no worktreePath", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItem = container.querySelector(".branchItem")!;
			fireEvent.contextMenu(branchItem, { clientX: 100, clientY: 200 });

			const contextMenu = container.querySelector(".menu");
			const items = contextMenu!.querySelectorAll(".item");
			const copyPathItem = Array.from(items).find((i) => i.querySelector(".label")?.textContent === "Copy Path")!;
			expect(copyPathItem.hasAttribute("disabled")).toBe(true);
		});

		it("does not show Delete Worktree for non-main branch without worktreePath", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/y": {
							name: "feature/y",
							isMain: false,
							worktreePath: null,
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItems = container.querySelectorAll(".branchItem");
			fireEvent.contextMenu(branchItems[1], { clientX: 100, clientY: 200 });

			const contextMenu = container.querySelector(".menu");
			const items = contextMenu!.querySelectorAll(".item");
			// Copy Path, Add Terminal, Set Label…, Rename Branch (NO Delete Worktree)
			expect(items.length).toBe(4);
			const labels = Array.from(items).map((i) => i.querySelector(".label")!.textContent);
			expect(labels).not.toContain("Delete Worktree");
		});

		it("shows Delete Worktree option for non-main branch context menu", () => {
			setRepos({
				"/repo1": makeRepo({
					branches: {
						main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
						"feature/x": {
							name: "feature/x",
							isMain: false,
							worktreePath: "/wt/x",
							terminals: [],
							additions: 0,
							deletions: 0,
						},
					},
				}),
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const branchItems = container.querySelectorAll(".branchItem");
			// feature/x is second (sorted after main)
			fireEvent.contextMenu(branchItems[1], { clientX: 100, clientY: 200 });

			const contextMenu = container.querySelector(".menu")!;
			// Branch git ops were condensed into a "Branch ›" submenu (commit 37d95eaa),
			// so top-level is Copy Path, Add Terminal, Set Label, Branch — and
			// Delete Worktree lives inside the Branch submenu, not at top level.
			const topLabels = Array.from(contextMenu.querySelectorAll(".item")).map(
				(i) => i.querySelector(".label")!.textContent,
			);
			expect(topLabels).toContain("Branch");
			expect(topLabels).not.toContain("Delete Worktree");

			// Open the "Branch" submenu (hover) and assert Delete Worktree is inside.
			const branchWrap = Array.from(contextMenu.querySelectorAll(".itemWrap")).find(
				(w) => w.querySelector(".label")!.textContent === "Branch",
			);
			expect(branchWrap).toBeTruthy();
			fireEvent.mouseEnter(branchWrap!);
			const submenu = contextMenu.querySelector(".submenu")!;
			const subLabels = Array.from(submenu.querySelectorAll(".item")).map(
				(i) => i.querySelector(".label")!.textContent,
			);
			expect(subLabels).toContain("Delete Worktree");
		});
	});

	describe("group sections", () => {
		it("renders group headers with name and chevron", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".groupHeader");
			expect(header).not.toBeNull();
			expect(container.querySelector(".groupName")!.textContent).toBe("Work");
			expect(container.querySelector(".groupChevron")).not.toBeNull();
		});

		it("renders repos inside group", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const groupSection = container.querySelector(".groupSection");
			expect(groupSection).not.toBeNull();
			const repoSections = groupSection!.querySelectorAll(".repoSection");
			expect(repoSections.length).toBe(1);
		});

		it("clicking group header toggles collapsed", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".groupHeader")!;
			fireEvent.click(header);
			expect(mockToggleGroupCollapsed).toHaveBeenCalledWith("g1");
		});

		it("collapsed group hides repos", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: true, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const groupRepos = container.querySelector(".groupRepos");
			expect(groupRepos).toBeNull();
		});

		it("group color dot renders when color is set", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "#4A9EFF", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const dot = container.querySelector(".groupColorDot");
			expect(dot).not.toBeNull();
		});

		it("group color dot does not render when no color", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const dot = container.querySelector(".groupColorDot");
			expect(dot).toBeNull();
		});

		it("repo count badge shows correct number", () => {
			const repo1 = makeRepo();
			const repo2 = makeRepo({ path: "/repo2", displayName: "Repo Two", initials: "RT" });
			setRepos({ "/repo1": repo1, "/repo2": repo2 });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1", "/repo2"] },
						repos: [repo1, repo2],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const count = container.querySelector(".groupCount");
			expect(count).not.toBeNull();
			expect(count!.textContent).toBe("2");
		});

		it("groups render before ungrouped repos", () => {
			const repo1 = makeRepo();
			const repo2 = makeRepo({ path: "/repo2", displayName: "Repo Two", initials: "RT" });
			setRepos({ "/repo1": repo1, "/repo2": repo2 });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo1],
					},
				],
				ungrouped: [repo2],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoList = container.querySelector(".repoList")!;
			const groupSection = repoList.querySelector(".groupSection");
			const repoSections = repoList.querySelectorAll(":scope > .repoSection");
			// Group section should exist
			expect(groupSection).not.toBeNull();
			// Ungrouped repo should render as direct child repo-section
			expect(repoSections.length).toBe(1);
		});

		it("empty group shows placeholder text", () => {
			setRepos({});
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Empty Group", color: "", collapsed: false, repoOrder: [] },
						repos: [],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const hint = container.querySelector(".groupEmptyHint");
			expect(hint).not.toBeNull();
		});

		it("group header right-click shows Rename, Change Color, Delete", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".groupHeader")!;
			fireEvent.contextMenu(header, { clientX: 100, clientY: 200 });
			const menu = container.querySelector(".menu");
			expect(menu).not.toBeNull();
			const labels = Array.from(menu!.querySelectorAll(".label")).map((el) => el.textContent);
			expect(labels).toContain("Rename Group");
			expect(labels).toContain("Change Color");
			expect(labels).toContain("Delete Group");
		});

		it("delete group calls deleteGroup()", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".groupHeader")!;
			fireEvent.contextMenu(header, { clientX: 100, clientY: 200 });
			const menuItems = container.querySelectorAll(".menu .item");
			const deleteItem = Array.from(menuItems).find(
				(el) => el.querySelector(".label")?.textContent === "Delete Group",
			)!;
			fireEvent.click(deleteItem);
			expect(mockDeleteGroup).toHaveBeenCalledWith("g1");
		});

		it("repo header right-click includes Move to Group with submenu", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: [] },
						repos: [],
					},
				],
				ungrouped: [repo],
			});
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const header = container.querySelector(".repoHeader")!;
			fireEvent.contextMenu(header, { clientX: 100, clientY: 200 });
			const labels = Array.from(container.querySelectorAll(".label")).map((el) => el.textContent);
			expect(labels).toContain("Move to Group");
		});

		it("no groups = flat list behavior (backward compatible)", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			// setRepos defaults to all ungrouped, which is correct here
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			// No group sections
			expect(container.querySelector(".groupSection")).toBeNull();
			// But repos still render
			expect(container.querySelectorAll(".repoSection").length).toBe(1);
		});
	});

	describe("drag-and-drop (pointer-based)", () => {
		it("pointerDown on repo section initiates drag after movement threshold", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoSection = container.querySelector(".repoSection")!;

			// pointerDown alone should not add dragging class
			fireEvent.pointerDown(repoSection, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
			expect(repoSection.classList.contains("dragging")).toBe(false);

			// Move past threshold — dragging class appears
			fireEvent.pointerMove(document, { pointerId: 1, clientX: 20, clientY: 10 });
			expect(repoSection.classList.contains("dragging")).toBe(true);

			// Cleanup
			fireEvent.pointerUp(document, { pointerId: 1, clientX: 20, clientY: 10 });
			expect(repoSection.classList.contains("dragging")).toBe(false);
		});

		it("pointerDown on group header initiates group drag after threshold", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			mockGetGroupedLayout.mockReturnValue({
				groups: [
					{
						group: { id: "g1", name: "Work", color: "", collapsed: false, repoOrder: ["/repo1"] },
						repos: [repo],
					},
				],
				ungrouped: [],
			});

			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const groupHeader = container.querySelector(".groupHeader")!;

			fireEvent.pointerDown(groupHeader, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
			fireEvent.pointerMove(document, { pointerId: 1, clientX: 20, clientY: 10 });

			// Ghost element should exist in the document
			const ghosts = document.querySelectorAll("[style*='position: fixed']");
			expect(ghosts.length).toBeGreaterThan(0);

			// Cleanup
			fireEvent.pointerUp(document, { pointerId: 1, clientX: 20, clientY: 10 });
		});

		it("escape cancels drag without performing any action", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoSection = container.querySelector(".repoSection")!;

			fireEvent.pointerDown(repoSection, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
			fireEvent.pointerMove(document, { pointerId: 1, clientX: 20, clientY: 10 });
			fireEvent.keyDown(document, { key: "Escape" });

			expect(mockReorderRepo).not.toHaveBeenCalled();
			expect(mockReorderRepoInGroup).not.toHaveBeenCalled();
			expect(mockMoveRepoBetweenGroups).not.toHaveBeenCalled();
			expect(mockAddRepoToGroup).not.toHaveBeenCalled();
			expect(mockRemoveRepoFromGroup).not.toHaveBeenCalled();
		});

		it("right-click does not initiate drag", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoSection = container.querySelector(".repoSection")!;

			// button=2 is right-click
			fireEvent.pointerDown(repoSection, { button: 2, pointerId: 1, clientX: 10, clientY: 10 });
			fireEvent.pointerMove(document, { pointerId: 1, clientX: 20, clientY: 10 });

			expect(repoSection.classList.contains("dragging")).toBe(false);
			fireEvent.pointerUp(document, { pointerId: 1, clientX: 20, clientY: 10 });
		});

		it("pointerUp without crossing threshold is a click, not a drag", () => {
			const repo = makeRepo();
			setRepos({ "/repo1": repo });
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const repoSection = container.querySelector(".repoSection")!;

			fireEvent.pointerDown(repoSection, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
			// Move less than threshold (5px)
			fireEvent.pointerMove(document, { pointerId: 1, clientX: 12, clientY: 10 });
			fireEvent.pointerUp(document, { pointerId: 1, clientX: 12, clientY: 10 });

			// No drag actions
			expect(mockReorderRepo).not.toHaveBeenCalled();
			expect(repoSection.classList.contains("dragging")).toBe(false);
		});
	});

	describe("resize handle", () => {
		it("renders a resize handle element", () => {
			const { container } = render(() => <Sidebar {...defaultProps()} />);
			const handle = container.querySelector(".resizeHandle");
			expect(handle).not.toBeNull();
		});

		it("applies width from ui store", () => {
			uiStore.setSidebarWidth(350);
			render(() => <Sidebar {...defaultProps()} />);
			expect(document.documentElement.style.getPropertyValue("--sidebar-width")).toBe("350px");
		});

		it("clamps width to min/max via ui store", () => {
			uiStore.setSidebarWidth(100);
			render(() => <Sidebar {...defaultProps()} />);
			expect(document.documentElement.style.getPropertyValue("--sidebar-width")).toBe("200px");
		});
	});

	describe("PrDetailPopover auto-show vs manual click", () => {
		it("PR badge click on non-active repo opens PrDetailPopover without auto-show closing it", async () => {
			// Two repos: /repo1 is active (no PR), /repo2 has a branch with a conflicting PR
			setRepos(
				{
					"/repo1": makeRepo({ path: "/repo1" }),
					"/repo2": makeRepo({
						path: "/repo2",
						displayName: "Repo Two",
						initials: "RT",
						branches: {
							main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
							feature: {
								name: "feature",
								isMain: false,
								worktreePath: "/repo2-wt",
								terminals: [],
								additions: 4,
								deletions: 0,
							},
						},
					}),
				},
				"/repo1",
			);

			mockGetActive.mockReturnValue({ path: "/repo1", activeBranch: "main" });

			// PR data only for /repo2/feature
			mockGetPrStatus.mockImplementation((...args: unknown[]) => {
				if (args[0] === "/repo2" && args[1] === "feature") {
					return {
						state: "OPEN",
						number: 99,
						title: "Feature PR",
						url: "https://example.com",
						mergeable: "CONFLICTING",
					};
				}
				return null;
			});

			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// The Conflicts badge should render for /repo2/feature
			const badge = container.querySelector("[title='PR #99']");
			expect(badge).not.toBeNull();

			// Click the badge's parent span (which has the onClick handler)
			await fireEvent.click(badge!.parentElement!);

			// PrDetailPopover should be rendered and not immediately closed by auto-show effect
			const popover = container.querySelector("[data-testid='pr-detail-popover']");
			expect(popover).not.toBeNull();
			expect(popover!.getAttribute("data-branch")).toBe("feature");
			expect(popover!.getAttribute("data-repo")).toBe("/repo2");
		});

		it("auto-shows PrDetailPopover when active branch has an open PR", async () => {
			mockGetActive.mockReturnValue({ path: "/repo1", activeBranch: "main" });
			mockGetPrStatus.mockImplementation((...args: unknown[]) => {
				if (args[0] === "/repo1" && args[1] === "main") {
					return { state: "OPEN", number: 7, title: "Auto PR", url: "https://example.com" };
				}
				return null;
			});
			setRepos({ "/repo1": makeRepo() });

			const { container } = render(() => <Sidebar {...defaultProps()} />);
			// setPrDetailTarget is deferred via queueMicrotask to avoid blocking branch-switch flush
			await new Promise<void>((r) => queueMicrotask(r));
			const popover = container.querySelector("[data-testid='pr-detail-popover']");
			expect(popover).not.toBeNull();
			expect(popover!.getAttribute("data-branch")).toBe("main");
			expect(popover!.getAttribute("data-repo")).toBe("/repo1");
		});

		it("manual popover stays open during branch switch (closed only via explicit close)", async () => {
			// Two repos: /repo1 active with a PR, /repo2 has PR on feature
			setRepos(
				{
					"/repo1": makeRepo({ path: "/repo1" }),
					"/repo2": makeRepo({
						path: "/repo2",
						displayName: "Repo Two",
						initials: "RT",
						branches: {
							main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
							feature: {
								name: "feature",
								isMain: false,
								worktreePath: "/repo2-wt",
								terminals: [],
								additions: 4,
								deletions: 0,
							},
						},
					}),
				},
				"/repo1",
			);

			mockGetActive.mockReturnValue({ path: "/repo1", activeBranch: "main" });
			mockGetPrStatus.mockImplementation((...args: unknown[]) => {
				if (args[0] === "/repo2" && args[1] === "feature") {
					return {
						state: "OPEN",
						number: 99,
						title: "Feature PR",
						url: "https://example.com",
						mergeable: "CONFLICTING",
					};
				}
				return null;
			});

			const { container } = render(() => <Sidebar {...defaultProps()} />);

			// Click badge on /repo2/feature (manual open)
			const badge = container.querySelector("[title='PR #99']");
			expect(badge).not.toBeNull();
			await fireEvent.click(badge!.parentElement!);

			// Manual popover should be open
			let popover = container.querySelector("[data-testid='pr-detail-popover']");
			expect(popover).not.toBeNull();
			expect(popover!.getAttribute("data-repo")).toBe("/repo2");

			// Simulate branch switch on the active repo — manual popover must survive
			mockGetActive.mockReturnValue({ path: "/repo1", activeBranch: "develop" });
			setRepos(
				{
					"/repo1": makeRepo({ path: "/repo1", activeBranch: "develop" }),
					"/repo2": makeRepo({
						path: "/repo2",
						displayName: "Repo Two",
						initials: "RT",
						branches: {
							main: { name: "main", isMain: true, worktreePath: null, terminals: [], additions: 0, deletions: 0 },
							feature: {
								name: "feature",
								isMain: false,
								worktreePath: "/repo2-wt",
								terminals: [],
								additions: 4,
								deletions: 0,
							},
						},
					}),
				},
				"/repo1",
			);

			// Manual popover should STILL be open after branch switch
			popover = container.querySelector("[data-testid='pr-detail-popover']");
			expect(popover).not.toBeNull();
			expect(popover!.getAttribute("data-repo")).toBe("/repo2");
		});
	});
});
