import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { FileEntry } from "./usePathCompletion";
import {
	getCurrentPartial,
	isFuzzyMatch,
	usePathCompletion,
} from "./usePathCompletion";

describe("isFuzzyMatch", () => {
	it("matches when query is empty", () => {
		expect(isFuzzyMatch("anything", "")).toBe(true);
	});

	it("matches exact prefix", () => {
		expect(isFuzzyMatch("Documents", "Doc")).toBe(true);
	});

	it("matches fuzzy subsequence", () => {
		expect(isFuzzyMatch("Documents", "dms")).toBe(true);
	});

	it("is case insensitive", () => {
		expect(isFuzzyMatch("Documents", "DOCU")).toBe(true);
	});

	it("rejects when chars not found in order", () => {
		expect(isFuzzyMatch("abc", "acb")).toBe(false);
	});

	it("rejects completely unrelated", () => {
		expect(isFuzzyMatch("hello", "xyz")).toBe(false);
	});
});

describe("getCurrentPartial", () => {
	it("returns empty for empty input", () => {
		expect(getCurrentPartial("")).toBe("");
	});

	it("returns empty when input ends with slash", () => {
		expect(getCurrentPartial("/home/user/")).toBe("");
	});

	it("returns last segment", () => {
		expect(getCurrentPartial("/home/user/Doc")).toBe("Doc");
	});

	it("returns full input when no slash", () => {
		expect(getCurrentPartial("Doc")).toBe("Doc");
	});

	it("trims then evaluates: trailing-slash input returns empty", () => {
		// "  /home/user/  " trims to "/home/user/" which ends with slash
		expect(getCurrentPartial("  /home/user/  ")).toBe("");
	});

	it("trims then returns last segment", () => {
		expect(getCurrentPartial("  /home/user/Doc  ")).toBe("Doc");
	});
});

describe("usePathCompletion hook", () => {
	const mockListDir = vi.fn<(path: string) => Promise<FileEntry[]>>();

	beforeEach(() => {
		mockListDir.mockReset();
	});

	it("returns empty matchList for empty input", async () => {
		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		// Allow effect + setTimeout(0) to fire
		await waitFor(() => {
			expect(mockListDir).not.toHaveBeenCalled();
		});

		expect(result.current.matchList).toEqual([]);
	});

	it("fetches directory listing and returns matches", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
			{ name: ".bashrc", kind: "file" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(2);
		});

		expect(result.current.matchList[0].name).toBe("Documents");
		expect(result.current.matchList[1].name).toBe("Downloads");
		expect(result.current.matchList.every((m) => m.path.endsWith("/"))).toBe(
			true,
		);
	});

	it("filters by fuzzy match on partial input", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
			{ name: "Desktop", kind: "dir" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/Doc",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		expect(result.current.matchList[0].name).toBe("Documents");
	});

	it("calls setPathInput when acceptMatch is invoked", async () => {
		const setPathInput = vi.fn();

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/",
				setPathInput,
				homeDir: "/home",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await act(async () => {
			result.current.acceptMatch("/home/user/Documents/");
		});

		expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
	});

	it("uses homeDir when input has no slash (bare partial)", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "Doc",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		expect(mockListDir).toHaveBeenCalledWith("/home/user");
		expect(result.current.matchList[0].name).toBe("Documents");
		expect(result.current.matchList[0].path).toBe("/home/user/Documents/");
	});

	it("returns early when input has no slash and homeDir is null", async () => {
		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "Doc",
				setPathInput: vi.fn(),
				homeDir: null,
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		// Wait for debounce + effect to fire
		await waitFor(() => {
			expect(result.current.matchList).toEqual([]);
		});

		expect(mockListDir).not.toHaveBeenCalled();
	});

	it("returns empty matchList when no dirs match the fuzzy filter", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/zzz",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(mockListDir).toHaveBeenCalled();
		});

		// No dirs match "zzz" fuzzy filter, so matchList stays empty
		expect(result.current.matchList).toEqual([]);
	});

	it("sets completionError when listDirectoryAbsolute throws an Error", async () => {
		const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
		mockListDir.mockRejectedValue(new Error("Permission denied"));

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/root/",
				setPathInput: vi.fn(),
				homeDir: null,
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.completionError).toBe("Permission denied");
		});

		expect(errorSpy).toHaveBeenCalledWith(new Error("Permission denied"));
		errorSpy.mockRestore();
	});

	it("sets generic completionError when listDirectoryAbsolute throws a non-Error", async () => {
		const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
		mockListDir.mockRejectedValue("some string error");

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/root/",
				setPathInput: vi.fn(),
				homeDir: null,
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.completionError).toBe(
				"Failed to compute suggestion.",
			);
		});

		expect(errorSpy).toHaveBeenCalledWith("some string error");
		errorSpy.mockRestore();
	});

	it("clears suggestionTail when selected match path does not start with input", async () => {
		mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "Doc",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		// Wait for matches to load (path will be /home/user/Documents/)
		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		// The match path is "/home/user/Documents/" which does NOT start with "Doc"
		// so suggestionTail should be ""
		expect(result.current.suggestionTail).toBe("");
	});

	it("acceptSelectedMatch calls setPathInput with the selected match path", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
		]);
		const setPathInput = vi.fn();

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/",
				setPathInput,
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(2);
		});

		await act(async () => {
			result.current.acceptSelectedMatch();
		});

		expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
	});

	it("acceptSelectedMatch does nothing when matchList is empty", async () => {
		const setPathInput = vi.fn();

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "",
				setPathInput,
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await act(async () => {
			result.current.acceptSelectedMatch();
		});

		expect(setPathInput).not.toHaveBeenCalled();
	});

	it("closeSuggestions clears matchList, selectedMatch, suggestionTail, and completionError", async () => {
		mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		await act(async () => {
			result.current.closeSuggestions();
		});

		expect(result.current.matchList).toEqual([]);
		expect(result.current.selectedMatch).toBe(0);
		expect(result.current.suggestionTail).toBe("");
		expect(result.current.completionError).toBeNull();
	});

	it("uses homeDir with trailing slash as-is", async () => {
		mockListDir.mockResolvedValue([{ name: "Projects", kind: "dir" }]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "Pro",
				setPathInput: vi.fn(),
				homeDir: "/home/user/",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		expect(mockListDir).toHaveBeenCalledWith("/home/user");
		expect(result.current.matchList[0].path).toBe("/home/user/Projects/");
	});

	it("handles root directory listing (dir = '/')", async () => {
		mockListDir.mockResolvedValue([
			{ name: "home", kind: "dir" },
			{ name: "etc", kind: "dir" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/",
				setPathInput: vi.fn(),
				homeDir: null,
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(2);
		});

		expect(mockListDir).toHaveBeenCalledWith("/");
		expect(result.current.matchList[0].name).toBe("etc");
		expect(result.current.matchList[1].name).toBe("home");
	});

	it("computes suggestionTail when match path starts with trimmed input", async () => {
		mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(1);
		});

		// path is "/home/user/Documents/" and input is "/home/user/"
		// so tail should be "Documents/"
		expect(result.current.suggestionTail).toBe("Documents/");
	});

	it("setSelectedMatch updates the selected index", async () => {
		mockListDir.mockResolvedValue([
			{ name: "Documents", kind: "dir" },
			{ name: "Downloads", kind: "dir" },
		]);

		const { result } = renderHook(() =>
			usePathCompletion({
				pathInput: "/home/user/",
				setPathInput: vi.fn(),
				homeDir: "/home/user",
				listDirectoryAbsolute: mockListDir,
				debounceMs: 0,
			}),
		);

		await waitFor(() => {
			expect(result.current.matchList.length).toBe(2);
		});

		await act(async () => {
			result.current.setSelectedMatch(1);
		});

		expect(result.current.selectedMatch).toBe(1);
		// After selecting index 1, suggestionTail should reflect "Downloads/"
		expect(result.current.suggestionTail).toBe("Downloads/");
	});
});
